Skip to content
Open
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
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Moose v0.8.0 is an open source AI agent for iOS and Android powered by Pi Durabl

Requires Node.js 22.19 or later. The Moose app itself does not run Node.js. Node is used for development, builds, and tests, and by the optional companion service inside Termux.

Tests also require JDK 17 or later (`java` and `javac` on PATH) to compile and exercise the Android wake-lock lifecycle without an emulator.

```sh
npm ci
npm run ios
Expand Down Expand Up @@ -82,7 +84,7 @@ Automated tests use controlled responses, without signing into real accounts or

Recurring tasks run immediately for the first iteration, with an interval of at least one minute and a total of 2–100 iterations. Requests receive a stable ID before submission. Unconfirmed submissions can retry the same ID to avoid duplicate queue entries. Stopping a recurring task stops future scheduling; an already submitted response must be stopped separately in the conversation.

Android protects user-initiated finite runs with a bounded foreground service; see [Android foreground execution](#android-foreground-execution). iOS may pause when suspended. Exact recurring execution times are not guaranteed. Reopening recovers committed state, and an overdue recurring task runs once before calculating its next interval. This version has no remote execution host or cross-device synchronization.
Android protects user-initiated finite runs with a foreground service; see [Android foreground execution](#android-foreground-execution). iOS may pause when suspended. Exact recurring execution times are not guaranteed. Reopening recovers committed state, and an overdue recurring task runs once before calculating its next interval. This version has no remote execution host or cross-device synchronization.

Restoring a checkpoint creates a new branch from the corresponding historical message and preserves the original conversation.

Expand Down Expand Up @@ -241,7 +243,7 @@ npx expo-doctor
Current validation coverage:

- iOS: Native debug build, installation, and operation on an iPhone 17 Pro simulator. Checks covered chat layout, message menus, keyboard avoidance, skill downloads and persistence, and local authentication callback listening and cancellation cleanup.
- Android: Debug APK compilation, installation on an Android 35 emulator, and controlled foreground-service SSE / local file-tool / next-round execution with SQLite commits, Activity destruction/recreation, notification stopping, and force-stop persistence passed. Physical-device Doze, vendor power policies, the full 30-minute timeout, and real Termux operations remain unverified. Release assets exported successfully. [GitHub Actions successfully compiled a test APK](https://github.com/sugarforever/moose/actions/runs/39503789329) containing the native Termux setup module. Phone installation, permission grants, and real Termux operations have not yet been validated.
- Android: Debug APK compilation, installation on an Android 35 emulator, and controlled foreground-service SSE / local file-tool / next-round execution with SQLite commits, Activity destruction/recreation, notification stopping, and force-stop persistence passed. Physical-device Doze, vendor power policies, long-duration physical-device execution and real Termux operations remain unverified. Release assets exported successfully. [GitHub Actions successfully compiled a test APK](https://github.com/sugarforever/moose/actions/runs/39503789329) containing the native Termux setup module. Phone installation, permission grants, and real Termux operations have not yet been validated.
- Expo Doctor: 19 of 20 checks passed. The remaining check concerns new-architecture metadata for the TCP library and missing public-directory metadata for the local Termux and iSH modules; this is not a fully passing result.
- File environment: The local package completed real file reads, writes, edits, SQLite reopening, and workspace isolation checks on the iOS simulator using controlled OpenAI SSE responses, without real credentials or billable requests.

Expand Down Expand Up @@ -274,11 +276,11 @@ See [AGENTS.md](AGENTS.md) for development constraints and [chat layout notes](d

On Android, a user-initiated answer or finite task establishes a shared foreground service and notification before model work begins. The existing Hermes runtime and Pi Harness continue the same SSE, local tools and SQLite commits when the Activity is hidden. Returning to the app does not resubmit, reopen the Harness or resume it again. Notification permission is required; a denied permission or failed service handshake prevents model execution.

Automatic compaction runs as part of the active answer rather than as speculative background work. All active conversations share the service. The last run releases it; each continuous service generation has a 30-minute execution budget and a bounded partial wake lock. The notification's Stop action cancels all answers and recurring tasks, including future iterations. If JavaScript cannot acknowledge cancellation within 10 seconds, the native service terminates this process; a persistent cancellation marker is processed before recovery on the next manual launch. A missing startup handshake triggers cancellation after 15 seconds.
Automatic compaction runs as part of the active answer rather than as speculative background work. All active conversations share the service. The last run releases it. Moose imposes no total execution-time limit; a timed partial wake lock is renewed every five minutes while the service is active, with each acquisition expiring after ten minutes. Completion, Stop and service destruction cancel renewal and release the lock. The notification's Stop action cancels all answers and recurring tasks, including future iterations. If JavaScript cannot acknowledge cancellation within 10 seconds, the native service terminates this process; a persistent cancellation marker is processed before recovery on the next manual launch. A missing startup handshake triggers cancellation after 15 seconds.

Recurring tasks do not hold the service while waiting. An iteration due while the app is hidden waits until Moose is visible unless another active run already holds the service. There is no scheduled background wakeup, boot receiver or automatic service restart. A tool requiring new permission stores the pending permission scope and prompts the user to open Moose; it never grants permission automatically. Termux is a separate app and is not kept alive by Moose's service. Switching execution environments is blocked while any conversation, repeat, pending request or foreground admission is active.

Chat shows the current task separately from historical reply attempts: running, retrying, waiting for network or authorization, completed, failed or stopped. Tap the task status or an attempt's Details for bounded, credential-redacted errors, stage, time and retry count. Native notification stop, budget expiry, startup timeout, permission loss, runtime destruction and service destruction keep distinct cancellation reasons. These diagnostics are persisted with the conversation (latest 200 events); they are separate from session-only Termux transport diagnostics.
Chat shows the current task separately from historical reply attempts: running, retrying, waiting for network or authorization, completed, failed or stopped. Tap the task status or an attempt's Details for bounded, credential-redacted errors, stage, time and retry count. Native notification stop, wake-lock renewal failure, startup timeout, permission loss, runtime destruction and service destruction keep distinct cancellation reasons. Historical budget-expiry reasons remain readable, but new runs are not cancelled by a total-duration timer. These diagnostics are persisted with the conversation (latest 200 events); they are separate from session-only Termux transport diagnostics.

Transient model failures use Pi's durable retry checkpoint, with at most three generation retries and exponential 2/4/8-second backoff. Reopening projects committed messages and tool results and reconciles already-settled outbox items; it does not resubmit a task on foreground return. A failed attempt that later recovered is labelled as history. Network waiting is inferred from a transport error with a scheduled retry, rather than a claim that connectivity has been measured. Authentication and quota errors are not retried automatically. An operation whose effects are unknown blocks further mutations across restart and context compaction; inspect its effects before choosing **Review interrupted operation → Allow further changes**. That action does not rerun the command. See [background recovery design and validation](docs/background-recovery.md).

Expand Down
15 changes: 15 additions & 0 deletions docs/android-execution-duration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Android execution duration

## Decision

User-started Android agent runs have no Moose-imposed total duration limit. Remove the service's 30-minute cancellation timer and its notification/manifest wording. Keep the existing shared foreground service, notification permission requirement, 15-second startup handshake, explicit Stop action, 10-second cancellation acknowledgement deadline, and persistent stop reasons.

The partial wake lock remains time-bounded: acquire it for 10 minutes and renew every 5 minutes while this service generation is active. Service shutdown cancels renewal and releases the lock. A renewal failure stops renewal and enters the existing durable cancellation path with `wake_lock_failed`; it does not silently run without CPU protection. Renewal uses Android's main Handler and uptime clock, independently of React view updates. No new agent scheduler or model retry policy is introduced.

Waiting recurring tasks still release the service. There is no scheduled wakeup or automatic process restart. Android may suspend, restrict networking, or terminate the application; removing a local timer does not override those rules. Historical `budget_expired` diagnostics remain readable, but new service generations no longer produce that reason.

## Verification

Compile and execute the production wake-lock lifecycle helper on the JVM with a controlled clock, covering operation beyond 30 minutes, renewal before lock expiry, explicit stop, late callbacks and acquisition failure. These tests require JDK 17 or newer and run within `npm test`. Keep the existing execution-owner and durable cancellation regression tests.

Run the complete project verification. Android native compilation and device operation are separate evidence. On a physical phone, use a controlled task lasting over 35 minutes, verify completion before reopening, then check Stop and final resource release. No paid inference is required for controlled verification.
8 changes: 7 additions & 1 deletion docs/background-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Locking the screen must not cancel a run. Keep the existing process-lifetime Har

- Keep a bounded conversation-scoped `moose.execution` document (200 events) for response failures, generation restarts and explicit cancellation. Record task ID, phase, timestamp, attempt/retry count, stop reason and sanitized error. The transcript remains the source of final results and keeps older errors beyond the diagnostic window. Never store headers, tokens, request bodies or tool arguments in this document.
- Derive current status from committed `pi.live`, transcript and execution diagnostics. Approval waiting overrides running. A failed attempt followed by successful generation in the same user turn is historical recovery, not a failed current task. Details remain accessible beside the attempt.
- Persist cancellation intent before aborting. Android preserves the first native cancellation reason and time before emitting the bridge event; next launch consumes pending cancellation before scheduling. Distinguish notification stop, budget expiry, handshake timeout, notification permission loss, runtime destruction and unexpected service destruction. Abrupt process death cannot reliably identify an OS reason; report runtime restart without inventing one.
- Persist cancellation intent before aborting. Android preserves the first native cancellation reason and time before emitting the bridge event; next launch consumes pending cancellation before scheduling. Distinguish notification stop, wake-lock renewal failure, handshake timeout, notification permission loss, runtime destruction and unexpected service destruction. Historical budget-expiry reasons remain readable; new runs have no total-duration cancellation timer. Abrupt process death cannot reliably identify an OS reason; report runtime restart without inventing one.
- Use Pi's generation retry checkpoint, explicitly bounded to three retries with 2/4/8 second backoff (30 second delay ceiling). Authentication, quota and cancellation are terminal. Recovery reads committed history/tool results; it does not resume an SSE byte offset. Retain Pi's unsafe tool replay protection, and prevent a model from bypassing it with another mutating call after an unknown outcome. The conversation retains an independent unknown-effect latch across compaction and reopening. Only the explicit “Allow further changes” action clears it; a new user message does not clear it, and the action itself never reruns a tool.
- Committed views drive UI, including after reopening. Diagnostic hooks commit before the provider outcome is classified. No UI callback owns execution or persistence. Terminal outcome remains authoritative even when UI detaches.

Expand Down Expand Up @@ -72,3 +72,9 @@ The standalone cold-route regression initially opened saved chat 367 instead of
To repeat the cold regression on a release APK with the controlled BackgroundSmoke database selected, run `ADB=/path/to/adb node scripts/notification-route-smoke.mjs 367 379`, substituting two known controlled probe IDs. The script requires the source fixture on screen and no active execution service before ending the process, retains the recent task, and verifies that the requested saved probe appears after cold launch.

After rebasing onto the independently merged iOS iSH integration (`78eaeea`), the complete verification passed again: 245 tests passed, 37 skipped (including the environment-gated embedded iSH suite), 3 bootstrap tests passed, portable smoke and both Hermes exports passed, and Expo Doctor passed 20/20. No iSH native or iOS background-execution validation is claimed by this notification change.

## Execution duration change, 2026-10-25

The foreground service no longer cancels active tasks at 30 minutes. Its partial wake lock is acquired with a ten-minute expiry and renewed every five minutes until the last execution lease releases the service. A failed renewal enters durable cancellation as `wake_lock_failed`. Startup handshake, notification permission and explicit cancellation behavior remain unchanged. Waiting recurring tasks still do not hold the service, and automatic process restart is not introduced.

See [duration design and acceptance](android-execution-duration.md). The earlier native-budget validation notes above describe previous versions. Device execution beyond 30 minutes must be validated separately; JVM controlled-clock tests do not establish Android screen-off behavior.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<service android:name="dev.verysmallwoods.background.MooseExecutionService"
android:exported="false" android:foregroundServiceType="specialUse" android:stopWithTask="false">
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Continue user-initiated finite AI conversations: consume streaming responses, execute local tools and commit SQLite while the Activity is hidden. One shared service, visible stop action, maximum 30 minutes; no scheduled wakeup or automatic restart." />
android:value="Continue user-initiated AI conversations: consume streaming responses, execute local tools and commit SQLite while the Activity is hidden. One shared service with a visible stop action, released when active runs finish; no scheduled wakeup or automatic restart." />
</service>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package dev.verysmallwoods.background;

/** Execution lock lifecycle, with Android operations supplied by the service. */
final class ExecutionWakeLock {
private static final long LOCK_TIMEOUT_MS = 10L * 60 * 1000;
private static final long RENEW_AFTER_MS = 5L * 60 * 1000;
interface Scheduler {
void postDelayed(Runnable callback, long delay);
void removeCallbacks(Runnable callback);
}
interface Lock {
void acquire(long timeout);
void release();
}
private final Scheduler scheduler;
private final Lock lock;
private final Runnable onFailure;
private boolean active;
private long generation;
private Runnable renewal;

ExecutionWakeLock(Scheduler scheduler, Lock lock, Runnable onFailure) {
this.scheduler = scheduler;
this.lock = lock;
this.onFailure = onFailure;
}
void start() {
if (active) return;
active = true;
long current = ++generation;
renewal = () -> {
if (!active || generation != current) return;
try {
lock.acquire(LOCK_TIMEOUT_MS);
scheduler.postDelayed(renewal, RENEW_AFTER_MS);
} catch (RuntimeException error) {
stop();
onFailure.run();
}
};
try {
lock.acquire(LOCK_TIMEOUT_MS);
scheduler.postDelayed(renewal, RENEW_AFTER_MS);
} catch (RuntimeException error) {
stop();
throw error;
}
}
void stop() {
if (!active) return;
active = false;
scheduler.removeCallbacks(renewal);
lock.release();
}
}
Loading
Loading