Skip to content

Add scheduled loops for recurring prompt execution#52

Closed
zmanian wants to merge 28 commits intomcintyre94:mainfrom
zmanian:scheduled-loops
Closed

Add scheduled loops for recurring prompt execution#52
zmanian wants to merge 28 commits intomcintyre94:mainfrom
zmanian:scheduled-loops

Conversation

@zmanian
Copy link
Contributor

@zmanian zmanian commented Mar 7, 2026

Summary

Client-side recurring prompt execution ("loops") for babysitting PRs and collaborator interactions. Long-press the send button to create a loop with configurable intervals (5m-1h) and durations (1d-1mo). Loops appear in a new dashboard section with pause/resume/delete controls and full iteration history.

How it works

  • Creation: Long-press Send in any chat to create a loop with the current prompt, sprite, and working directory
  • Execution: Each iteration is a fresh claude -p call (no session resumption). Sprite is woken automatically if cold.
  • Results: Local notifications for each iteration + browsable chat transcript history in loop detail view
  • Management: Dashboard section with swipe actions (pause/resume/delete) and context menus
  • Expiry: Loops auto-stop after the configured duration (default 1 week)
  • Background: BGAppRefreshTask runs iterations at iOS's discretion when app is backgrounded

Other changes

  • Switched from generated Info.plist to custom Wisp/Info.plist for proper BGTaskSchedulerPermittedIdentifiers array support
  • Background fetch capability enabled

New files

  • Wisp/Models/Local/LoopModels.swift -- SwiftData models (SpriteLoop, LoopIteration, enums)
  • Wisp/Services/LoopManager.swift -- Timer-based foreground + BGTask background execution
  • Wisp/Services/NotificationService.swift -- Local notification helpers
  • Wisp/Views/Loop/CreateLoopSheet.swift -- Loop creation form
  • Wisp/Views/Loop/LoopRowView.swift -- Dashboard row component
  • Wisp/Views/Loop/LoopDetailView.swift -- Loop detail with iteration history
  • Wisp/Info.plist -- Custom Info.plist with background modes
  • WispTests/LoopModelTests.swift -- Model tests
  • WispTests/LoopManagerTests.swift -- Manager tests
  • WispTests/NotificationServiceTests.swift -- Notification tests

Test plan

  • Build succeeds on simulator
  • All tests pass
  • Long-press send button shows Create Loop sheet
  • Creating a loop starts first iteration immediately
  • Loop appears in dashboard Loops section
  • Pause/resume/delete via swipe and context menu
  • Loop detail shows iteration history with expandable transcripts
  • Local notifications fire for each iteration
  • Loop auto-stops at expiry

Generated with Claude Code

@zmanian zmanian force-pushed the scheduled-loops branch 2 times, most recently from 97a9f83 to 8a9c9ce Compare March 8, 2026 00:43
@mcintyre94
Copy link
Owner

This is a fun idea, I had the same thought about running cron type tasks on Sprites after seeing Openclaw and Claude Code's implementation. I do think cron and sprites is a really interesting design space, just not sure this app is the right place to actually kick off the tasks.

My hesitancy is that iOS background tasks seems like a really unstable foundation for cron jobs, how reliable are you finding the triggers in practice?

I really hope the Fly team might add native cron and then this app could just use that API to view/manage them.

zmanian and others added 27 commits March 15, 2026 12:51
Provides permission requesting, notification content building, posting,
and text truncation for loop completion summaries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds LoopState, LoopInterval, LoopDuration, IterationStatus enums and
SpriteLoop @model class with JSON-encoded iterations. Includes tests
for defaults, presets, expiration logic, and time remaining display.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Create the Loop creation sheet with sprite name, prompt, interval picker,
and duration picker. Add onLongPressSend callback to ChatInputBar with
long-press gesture on the send button for triggering loop creation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements the core LoopManager service with register, pause, resume,
stop, stopAll, and restoreLoops methods. Uses Timer-based scheduling
with a running iterations guard to prevent overlapping executions.
executeLoopPrompt is stubbed for wiring in Task 5.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the stub implementation with working code that:
- Wakes the sprite if not running (poll up to 60s)
- Builds a claude -p command with OAuth token
- Executes via streamService and parses NDJSON output
- Extracts assistant text blocks from Claude stream events
- Cleans up the service after completion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add LoopRowView with status indicator, prompt, sprite name, interval,
and time remaining. Add LoopDetailView with configuration section,
pause/resume/delete actions, and expandable iteration history. Add
Loops section to DashboardView with swipe actions and context menus
for pause/resume/delete. Also add missing files (LoopManager,
NotificationService, CreateLoopSheet, and new views) to Xcode project.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add UIBackgroundModes (fetch) and BGTaskSchedulerPermittedIdentifiers
to both Debug and Release build configurations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migrate from GENERATE_INFOPLIST_FILE to a custom Wisp/Info.plist to
properly declare BGTaskSchedulerPermittedIdentifiers as an array
(build settings only support a single string value).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Long-pressing the send button now presents the CreateLoopSheet,
which creates a SpriteLoop, persists it to SwiftData, and registers
it with the LoopManager to begin immediate execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…mage

SwiftUI Button consumes touches, preventing onLongPressGesture from firing.
Use separate onTapGesture and onLongPressGesture on an Image instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The handler closure was dispatched on an arbitrary background queue
(using: nil), causing a MainActor isolation assertion when constructing
@MainActor-isolated types (LoopManager, SpritesAPIClient) inside
Task { @mainactor in }. Using .main ensures the handler runs on the
main queue, matching the MainActor isolation requirements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents that handleBackgroundRefresh must be called from MainActor
context, matching the BGTaskScheduler handler's using: .main dispatch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fails fast with a clear message if this is ever called off the main
queue, rather than hitting an opaque Swift concurrency isolation crash.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change keychain accessibility from WhenUnlockedThisDeviceOnly to
AfterFirstUnlockThisDeviceOnly so background tasks can read API
tokens when the device is locked. Migrate existing items on launch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The runExec WebSocket call would fail immediately for cold/suspended
sprites, then the code would proceed to streamService on a sprite
that wasn't running yet. Now fire exec without awaiting, poll for
up to 90s, and fail explicitly if the sprite doesn't reach running.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the sprite just woke up, the first streamService call may fail
with "network connection lost" before any data is received. Retry
up to 3 times with 5s backoff for transient connection failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
503 errors mean the sprite isn't ready for service requests yet.
Increase backoff to 10/20/30s, bump max attempts to 5, and
re-verify sprite status before each retry attempt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the broken exec-based wake step from loops — the WebSocket
exec doesn't actually trigger wake for cold sprites. Instead, let
the streamService PUT (REST) trigger the wake; the existing retry
loop handles 503s during startup.

Also add retry logic to question tool install (up to 3 attempts
with 5s backoff) since the sprite may still be waking when the
first chat message is sent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
If the question tool fails to install (e.g. sprite still waking),
proceed with chat without the WispAsk MCP tool instead of showing
a fatal error that blocks all messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The flush path after stream completion only handled assistant events,
missing the result event where Claude's final response text lives.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The service log API sends stdout data as plain text strings, not
base64-encoded bytes. Data(base64Encoded:) was silently returning nil
for every chunk, causing all responses to be empty.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@zmanian
Copy link
Contributor Author

zmanian commented Mar 21, 2026

Closing — scheduled actions feature has been removed from zaki-main.

@zmanian zmanian closed this Mar 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants