Skip to content

feat(sync): add macos launchd sync scheduling - #309

Merged
samzong merged 2 commits into
mainfrom
feat/sync-schedule-launchd
Oct 14, 2026
Merged

samzong merged 2 commits into
mainfrom
feat/sync-schedule-launchd

Conversation

@samzong

@samzong samzong commented Oct 14, 2026

Copy link
Copy Markdown
Owner

Summary

Add recall sync schedule install|uninstall|status to manage a macOS user LaunchAgent running recall sync --project all. Install immediately kickstarts the first sync. Subsequent runs retain the requested interval, defaulting to 30 minutes. Other platforms reject these commands with a nonzero exit status.

The plist uses the absolute executable path, user log files, and AbandonProcessGroup=true so the embedding worker survives the sync process. Scheduled and manual syncs share the existing blocking lock; contention reports the lock owner's PID before waiting. Installation explains that macOS may request multiple folders in sequence: answer each prompt within 60 seconds, and reinstall to retry a failed first sync. Status reports the loaded job's last exit code, including the never-exited state.

Rationale

launchd owns scheduling without a resident Recall daemon. A manual plist template would use the same scheduler but leave executable-path changes, load state, and cleanup to users. The CLI manages those operations and retains existing manual sync behavior. The accepted freshness target is the configured interval, defaulting to 30 minutes while the logged-in Mac is awake.

Core sync behavior change: Git metadata probe timeout

  • Trigger: a repository-root or origin-URL metadata probe does not finish within 60 seconds. This affects manual and scheduled syncs on every platform, as well as read commands that infer a project using repository metadata.
  • Reason: a five-second deadline could terminate Recall before a user answered a macOS folder-access prompt, leaving an orphaned permission request. The 60-second deadline and immediate first run give users time to answer while Recall is running.
  • Consequence: the first timeout requests probe termination and stops the entire sync once cleanup completes, rather than continuing to wait on other directories. The failed sync releases its index lock. A timeout is an error, not a missing identity, and cannot overwrite an existing repository identity. Earlier successful writes remain committed. Ordinary non-Git directories and repositories without an origin retain their existing behavior.
  • Cleanup limitation: Recall requests termination and waits for the direct child. The wait has no separate deadline; uninterruptible kernel I/O, such as a stalled hard-mounted NFS share, can still hang cleanup and retain the lock. This is not a whole-sync or direct-filesystem deadline.
  • Recovery: the error identifies the probed directory. Check folder permissions, operating system privacy controls, and whether Git responds in that directory, then retry. Read commands can use --project all to bypass automatic project inference; this does not bypass sync probes. There is no automatic retry.
  • Read callers including search, session list, and export also receive timeout errors. TUI scope inference remains best effort and reports an inference error before its existing global fallback.
  • Implementation: capture stdout in a temporary file to avoid pipe-capacity deadlocks, suppress unused stderr, and use a monotonic deadline. Unix probes have their own process group; timeout cleanup requests termination of the group and waits for the direct child. Windows cleanup stops and waits for the direct child.

Test evidence

Toolchain: Recall 0.6.3, Cargo 1.96.0, cargo-audit 0.22.2, macOS 27.0.1 (26A434).
Final staged fingerprint: cd8a079c4075c9bf9504df65c624171bd9b0b0f8, base a1f0ba53e714f2de2add48b43040135b94662d3e.
The real-index acceptance artifact was built with cargo build --release from staged fingerprint 9d46ce43f5f0989cac1edf20b53587cd5c391556; binary SHA256 80cbfc8d42fdb93200465b97c4ca2a2c8bf3e770e6213b7c50c3893edf223b95.

  • Final artifact: cargo build --release exited 0 in 1m52s; SHA256 95ea2d30a5ae6dbd03219b6068de45275571b2c609391c5a010394dd843d0410. The final delta changes installation text, loaded-status reporting, documentation, and test duration; plist, kickstart, production timeout, and sync/error propagation are unchanged from the real-index acceptance artifact. The shared launchctl load query still treats exit 0 as loaded and 113 as missing.
  • Fresh make check: exit 0, All checks passed; dependency audit, formatting, workspace Clippy with --all-targets --features bench, and workspace tests passed (947 core tests).
  • Probe tests include a delayed successful response (50 ms response, 200 ms timeout), timed-out parent/child process-group cleanup with both PIDs absent, normal repositories, non-Git directories, and 256 KiB each of stdout/stderr without a pipe deadlock. The production 60-second deadline is exercised by the controlled release-CLI timeout below.
  • Isolated real release CLI with a temporary HOME and fixture index: a controlled hanging Git wrapper failed with exit 1 after 60.01 seconds. Exactly one probe ran, its PID was absent, the lock was released, and the stored identity was unchanged. A subsequent manual sync exited 0 and acquired the lock.
  • Earlier valid entry evidence for unchanged error propagation: backfill timeout retained partial identities; a concurrent manual sync printed the waiting PID and completed after lock release. Stalled root and origin probes reported their directory. search, session list, and export rejected failed inference and succeeded with --project all. These previous entry experiments used the former five-second threshold; they were not repeated at 60 seconds. Normal and non-Git directory behavior was exercised again by the final workspace tests.
  • Final timeout diagnostic: directory context followed by Git metadata probe timed out after 60s; it may be blocked by folder permissions or a stalled Git process. The operation was stopped; this is not evidence of index corruption. Check folder permissions, operating system privacy controls, and whether Git responds in the reported directory, then retry. Read commands can use `--project all` to bypass automatic project inference.
  • Real installation on the acceptance artifact: ./target/release/recall sync schedule install --interval 2m exited 0 and printed first sync started now; if macOS asks for folder access, click Allow within 60 seconds. launchctl print gui/501/io.github.samzong.recall.sync showed program = /Users/x/git/samzong/Recall/target/release/recall, arguments sync --project all, run interval = 120 seconds, and abandon process group. Status agreed: installed, loaded, binary exists.
  • First 60-second attempt failed: PID 52169 accepted Documents access (authValue=2) in approximately 0.74 seconds, then an unanswered Downloads prompt caused a root probe of /Users/x/Downloads to time out. The job exited 1 after 69.6 seconds, stdout was empty, and database mtime did not advance. The diagnostic is recorded at /tmp/recall-schedule-runtime-60s.log:110 and the Documents TCC result in /tmp/recall-schedule-tcc-60s-request.log. The acceptance watchdog had matched only Documents; its matching was expanded to all Recall folder-access prompts. That 0.74-second TCC record belongs to the failed run, not the successful one.
  • Immediate first run in the single retry using the unchanged binary: observed at 0.0 seconds, runs = 1. The successful run was PID 60057. At 108.9 seconds the job completed with last exit code = 0. Fresh stdout was 524 new, 8 updated, 28 backfilled, 8219 messages in 1m48s. Real database mtime advanced from 1791948681101686409 (2026-10-14T03:31:21.101686+00:00) to 1791953105009847701 (2026-10-14T04:45:05.009848+00:00). This was a normal incremental sync without --force; no additional installation or trigger was used.
  • Source warnings observed during the successful run: Kimi Code sessions directory absent, GS inventory unavailable, and Warp SQLite could not be opened read-only. The command exited 0 despite these warnings; successful scheduling does not establish complete coverage of unavailable session sources. These adapter conditions were not corrected in this change.
  • Cleanup: uninstall exited 0; launchctl print exited 113 with service not found; plist was absent; status returned Not installed, Loaded: no. Observed sync PID 60057 was absent. A nonblocking exclusive acquisition of the real sync.lock succeeded immediately and was released.
  • Final release CLI status against temporary native launchd fixtures running /bin/sh (no Recall sync): Last exit code: (never exited) before kickstart, then Last exit code: 0 and Last exit code: 7 matching native launchctl print. With no user plist, it accurately reported Not installed and Loaded: yes. Cleanup used the final CLI uninstall: native print returned 113, status reported Loaded: no without an exit-code field, and the real database mtime was unchanged. The real-index sync was not repeated after rebuilding.
  • Final installation text: First sync: started now; macOS may ask for access to multiple folders in sequence (Documents, Downloads, Desktop, or other volumes). Click Allow within 60 seconds for each prompt; if the first sync fails, rerun `recall sync schedule install`.

Unverified

  • complete coverage of the unavailable session sources, another interval after the immediate first run, Linux/Windows execution, Windows termination/reaping path, cleanup during uninterruptible kernel I/O (including hard-mounted NFS), sleep/logout behavior, or relocated executable behavior. Plist roundtrip tests ran only on this macOS machine.

Gate gaps

CI runs make check on Ubuntu. It does not exercise macOS plist roundtrips, launchd lifecycle and embedding-worker survival, sequential TCC permission prompts, real-index log/database updates, or loaded status output. Native acceptance remains a separate macOS check.

Windows probe termination and the non-macOS CLI entrypoints have no real-installation evidence here. Uninterruptible kernel-I/O cleanup, sleep/logout, and another scheduled interval also remain outside the executed checks. These coverage gaps require platform testing or explicit manual acceptance; a green unit-test run alone does not cover them.

Signed-off-by: samzong <samzong.lu@gmail.com>
@samzong

samzong commented Oct 14, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T05:20:59.607751Z bdeda77 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: bdeda77cbb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Signed-off-by: samzong <samzong.lu@gmail.com>
@samzong
samzong marked this pull request as ready for review October 14, 2026 05:25
@samzong
samzong merged commit 752a24e into main Oct 14, 2026
3 checks passed
@samzong
samzong deleted the feat/sync-schedule-launchd branch October 14, 2026 05:25
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.

1 participant