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
6 changes: 6 additions & 0 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ function MooseApp() {
const [history, setHistory] = useState(false);
const [artifactsVisible, setArtifactsVisible] = useState(false);
const [keyboardVisible, setKeyboardVisible] = useState(false);
useEffect(() => {
if (!moose.notificationNavigation) return;
setTab("chat");
setHistory(false);
setArtifactsVisible(false);
}, [moose.notificationNavigation]);
const followBottom = useRef(true);
const list = useRef<FlatList<ActivityGroupMessage>>(null);
const activityMessages = groupActivities(moose.messages, moose.activity);
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Moose

Moose v0.7.0 is an open source AI agent for iOS and Android powered by Pi Durable. Expo / React Native provides the interface, Pi Durable runs on the phone's Hermes runtime, and SQLite stores conversations and tasks. Model requests go directly to the provider; no Moose backend is required.
Moose v0.7.1 is an open source AI agent for iOS and Android powered by Pi Durable. Expo / React Native provides the interface, Pi Durable runs on the phone's Hermes runtime, and SQLite stores conversations and tasks. Model requests go directly to the provider; no Moose backend is required.

## Features

Expand Down Expand Up @@ -272,6 +272,8 @@ Chat shows the current task separately from historical reply attempts: running,

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).

Android’s ongoing notification shows committed execution progress. Background completion or failure posts one result alert per conversation; foreground results are silent. Tap a single-task or result notification to open its saved conversation without resubmitting it. Multiple active conversations share a count notification and the existing Stop-all action. Failure alerts use brief fixed categories without provider bodies or credentials. Task results can be muted separately in Android notification settings.

System reclamation, Doze, network loss and vendor power policies can still interrupt execution. Process death cannot preserve the original TCP connection or Promise, and recovery cannot guarantee a model is billed only once. Force-stop is respected. iOS retains its existing suspension behavior. The Android `specialUse` declaration describes finite AI work; this does not imply Google Play approval.

For a controlled native probe, rebuild the Android development client and run `EXPO_PUBLIC_MOOSE_BACKGROUND_SMOKE=1 npx expo start --dev-client`. Tap Run background probe, confirm the service starts, then press Home. After 15 seconds the probe should finish its file tool and second model round, commit SQLite and release its notification. The probe uses a separate database and controlled SSE, never real credentials or paid requests. Notification stopping can be checked during the delay. The same probe has a five/ten-minute lock test and a durable-results reader. For actual native network transport, start `node scripts/background-probe-server.mjs` on the development machine, then use **Run network probe** in the Android emulator; it connects only to the controlled local endpoint through `10.0.2.2:8106`. No request is forwarded to a model service.
7 changes: 4 additions & 3 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"expo": {
"name": "Moose",
"slug": "moose-mobile",
"version": "0.7.0",
"version": "0.7.1",
"scheme": "moose",
"orientation": "default",
"userInterfaceStyle": "light",
Expand All @@ -15,7 +15,7 @@
},
"android": {
"package": "dev.verysmallwoods.moose",
"versionCode": 11
"versionCode": 20
},
"plugins": [
"expo-sqlite",
Expand All @@ -32,7 +32,8 @@
],
"./plugins/termux-network.cjs",
"expo-sharing",
"./plugins/termux-setup.cjs"
"./plugins/termux-setup.cjs",
"./plugins/notification-intent.cjs"
],
"web": {
"bundler": "metro"
Expand Down
27 changes: 27 additions & 0 deletions docs/background-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,30 @@ To repeat: start the opt-in probe from README with a freshly loaded development
Native UI inspection used the opt-in preview with the actual Markdown component: recovered attempts used neutral text, terminal stopped attempts used red text, details expanded to show error/time/phase/retries, and task details opened a native alert. This was a controlled component preview, not a real-account chat.

Delivery checks: `npm run verify` passed with 226 tests and 8 existing skipped watcher cases, 3 Termux bootstrap tests, type checking, portable runtime smoke and Android/iOS Hermes exports. Android `assembleDebug` succeeded and that client was used for the native cases above. Independent reviews found and verified fixes for compacted-context replay protection, lifecycle-event association, cancelled retry counts and acknowledgement races. No native iOS build was performed.

## Task notifications

Android keeps one ongoing notification for the shared execution service. During admitted work it observes committed conversation views to show running, retrying, network retry waiting, or authorization waiting. A single conversation links directly to its chat; multiple active conversations show a count and open Moose without assigning the shared service to the wrong chat. Stop still cancels all tasks, and native stop causes and acknowledgement are unchanged.

After settlement, Moose rereads the final committed view before releasing the service. A changed completed or failed outcome posts one result notification per conversation on the **Task results** channel; overlapping leases are deduplicated. A final authorization error counts as a failed result, while an in-progress permission prompt remains waiting. Cancelled tasks do not alert. Unchanged historical outcomes never alert when reopening or retrying an already-settled request. Each repeat iteration is an admitted execution and may produce a result; sleeping intervals do not hold a service.

Result notifications are suppressed when the native Activity is resumed. Failure text uses fixed categories (credentials, network, usage limit, or generic failure) without copying provider bodies or user content. Tapping a result selects the existing persisted conversation through `moose://conversation/<id>`, including a cold launch; the route never submits a request. Invalid or deleted conversation targets are ignored.

Notifications remain required for Android admission. Denying permission prevents work from starting with an explicit error; losing permission retains native cancellation semantics. Muting the separate Task results channel silences results without changing task persistence. No notification permission or new background guarantee is added on iOS. This feature adds no executor, scheduler, or background wakeup.

### Notification acceptance, 2026-10-23

- `npm run verify` passed: 239 tests, 8 skipped, 3 real bootstrap-script tests, strict types, portable Pi/SQLite smoke, and Android/iOS Hermes exports. A first bootstrap process-lock test timed out; the standalone rerun and complete subsequent verify passed without changing that test. Final `npm test` again passed 239/8. Expo Doctor passed 20/20 and modified files passed Prettier.
- Android API 35 arm64 debug APK compiled and installed. The native compile caught a `Context`/`ReactContext` mismatch in the new bridge; the explicit checked cast was corrected and both subsequent native builds passed. Initial dependency-cache/download failures were resolved by fetching the missing dependencies; no dependency downgrade or source workaround was added.
- Controlled on-device SSE parsing, file write, second model round and SQLite settlement completed while Home was visible. The running notification was replaced by a conversation-specific completed result. A controlled terminal error containing a fixture token produced only the fixed generic failure notification. Existing historical failures produced no startup alerts.
- A controlled network failure displayed **Waiting for network. Retry scheduled.**, recovered in the same execution, then posted a completed result. Two simultaneous conversations shared one count notification with an untargeted app link, then produced distinct result tags for conversations 247 and 253; the service released after the last run.
- Tapping the actual notification **Stop** action cancelled two active probes (295 and 301), persisted `notification_stop` for both, released the service and produced no result alerts. Denied notification permission showed **Allow notifications to start background tasks** and did not reach model transport. Restoring permission allowed the next controlled run.
- Foreground failure (189) and foreground completion (335) produced no result notifications. A real single failure notification switched the already-running normal UI from another chat to saved conversation 367, without submitting work. Android's automatically generated collapsed group summary opens the app without choosing a conversation; individual task notifications retain their own targets.

These runs used isolated controlled model responses and SQLite fixtures, without real credentials or paid model calls. The emulator's unrelated saved Termux selection caused its existing connection error in the normal UI; this did not prevent reading or selecting the committed chat. No new Xiaomi/physical-device, Doze, full-budget, iOS native execution, or real account inference claim is made.

The standalone cold-route regression initially opened saved chat 367 instead of requested chat 379 after process death with a retained recent task. `plugins/notification-intent.cjs` now preserves the latest Activity intent before React's `onNewIntent` delegate runs; this keeps bootstrap URL lookup correct when the link arrives before JavaScript is ready. `scripts/notification-route-smoke.mjs` failed on the original standalone APK and passed after the fix (367 → 379, no execution service). This test ran offline and used the exact action/category/component/flags of the task notification's activity intent. Warm routing used a real app notification tap; cold routing used the equivalent activity intent because the development launcher intercepted the cold notification test. Neither is presented as physical-device validation.

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.
21 changes: 21 additions & 0 deletions docs/superpowers/plans/2026-10-23-background-notifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Background task notifications implementation plan

**Goal:** Show committed Android task progress and one result alert, with conversation routing.
**Architecture:** Extend the existing execution guard with conversation IDs. A small observer watches committed views during admitted work and reads the final view before releasing the existing service. Native code keeps the shared ongoing notification and posts per-conversation results on a separate alert channel. No executor or scheduler is added.
**Tech stack:** Pi Durable, Expo module, Android NotificationCompat, React Native Linking.

## Constraints and acceptance

- Preserve service admission, notification permission, Stop-all, cancellation acknowledgement and 30-minute budget.
- Observe only admitted work; an unchanged historical outcome cannot alert. Deduplicate overlapping leases for one conversation.
- Progress uses persisted state. Results are read after settlement before service release. Never include provider bodies or user text in notifications; failures use fixed categories.
- Multiple active conversations show a count without a misleading conversation target. Each completed conversation gets its own result target.
- Suppress result alerts while the Activity is resumed. Denied permission retains existing admission failure and loss cancellation semantics.
- Warm and cold URL handling validates an existing conversation and only selects it; no submission occurs. iOS gains no background guarantee.

## Work

- [x] Write failing observer tests covering persisted final reads, old failures, retries, overlapping leases, cancellation and bounded errors; implement the observer and guard ID propagation.
- [x] Extend native progress/result methods and notification intents, preserving stop causes. Add validated warm/cold conversation selection.
- [x] Run complete verify, native compile and controlled emulator probes; record evidence and limits.
- [ ] Review diff, create PR, fix findings/checks, merge; increment actual latest patch version, tag and inspect release APK/manifest/signature/companion.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ class MooseBackgroundModule : Module() {
Function("ready") { token: String -> MooseExecutionService.instance?.ready(token) }
AsyncFunction("stop") { token: String -> MooseExecutionService.releaseRequest(token) }
.runOnQueue(expo.modules.kotlin.functions.Queues.MAIN)
Function("waiting") { value: Boolean -> MooseExecutionService.instance?.waiting(value) }
Function("waiting") { id: Double, value: Boolean -> MooseExecutionService.instance?.waiting(id.toLong(), value) }
Function("progress") { items: List<Map<String, Any>> -> MooseExecutionService.instance?.progress(items) }
AsyncFunction("result") { id: Double, failed: Boolean, detail: String ->
MooseExecutionService.result(requireNotNull(appContext.reactContext as? com.facebook.react.bridge.ReactContext), id.toLong(), failed, detail)
}.runOnQueue(expo.modules.kotlin.functions.Queues.MAIN)
Function("pendingStop") { MooseExecutionService.pendingStop(requireNotNull(appContext.reactContext)) }
Function("stopDetail") { MooseExecutionService.stopDetail(requireNotNull(appContext.reactContext)) }
Function("acknowledgeStop") {
Expand Down
Loading