Files
AIChatExporter/docs/scheduling.md
T
JesseMarkowitzandClaude Opus 5 04ebb8987c release: v1.0.0, and split the README into docs/
The archive does what it was built to do, unattended, on both machines:
four providers, incremental and resumable, Markdown plus Joplin, on a
schedule, with results pushed to a phone and failures that announce
themselves. The one item left on the roadmap — a StartOS service that
consolidates both machines into one corpus — is an optional convenience,
not a gap. Hence 1.0.0 rather than another 0.x.

The code in this release shipped across b6ce636..8c3e00a: the claude.ai
403 session-invalid fix, the fork-subagent recursion fix, the FAILED-push
backstops on both platforms, and asset-download retries with cross-run
recovery, a circuit breaker and media counts in the notification. This
commit cuts the release and pays a documentation debt.

The README had reached 939 lines / 6,490 words / 48 KB / 96 headings,
doing four unrelated jobs at once, and FUTURE.md asked for the split to
ride along with the next substantial README edit rather than break every
existing anchor for its own sake. A release is that edit. README.md is
now 303 lines and keeps only what you need to install and configure the
tool; the rest moved verbatim into docs/providers.md, docs/cli.md,
docs/scheduling.md and docs/troubleshooting.md. No section was rewritten
or dropped and every original heading still exists, so each anchor still
resolves somewhere — but bookmarks into moved sections now need the
docs/ path, which is the cost this deferral was protecting against.

.gitignore needed the fix FUTURE.md predicted: the blanket *.md rule
(exported conversations are Markdown and may be private) matched all
four new pages, and git check-ignore confirmed every one would have been
silently ignored. !docs/*.md is added with a comment saying why. Gitea's
rendering of relative docs/ links cannot be checked before pushing, so
it still needs confirming after.

Also corrects two stale claims in the changelog: the Windows backstop is
no longer "not yet run under Windows PowerShell 5.1 or Task Scheduler"
(confirmed on winmini today, LastTaskResult 0 — only the FAILED push
itself remains unexercised there), and the test count is 414, not 409.

414 tests passing. No code changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gCWSknsWvhLnMFwa7qnGp
2026-10-20 22:18:37 -04:00

5.5 KiB

Running on a Schedule

Installing the daily run on Linux and Windows, what to know before relying on it, and how results reach your phone.

← README · Providers and Tokens · CLI Reference · Troubleshooting


Scheduling a Daily Run

sync chains export then joplin in one invocation, which is what a scheduler wants — one command, and a meaningful exit code so a failed run is visible instead of silent.

./ai-chat-exporter sync                      # every configured provider
./ai-chat-exporter sync --provider codex     # just one

Linux (systemd user timer)

./scheduling/install-systemd-timer.sh --provider claude-code --provider codex
./scheduling/install-systemd-timer.sh --uninstall

Defaults to 09:00 daily (--time 21:30 to change). Persistent=true means a machine that was off at the scheduled time runs the archive at next boot rather than skipping the day. To archive while logged out, loginctl enable-linger $USER.

Check on it with systemctl --user list-timers aichat-sync.timer and journalctl --user -u aichat-sync.service -n 50.

Windows (Task Scheduler)

.\scheduling\Register-AiChatSyncTask.ps1 -Provider chatgpt,claude
.\scheduling\Register-AiChatSyncTask.ps1 -Unregister

Per-user task, no admin rights needed. -StartWhenAvailable is the counterpart of systemd's Persistent=true. The task runs scheduling\run-sync.ps1 with its window hidden; Task Scheduler keeps no output, so each run's output is appended to cache\logs\scheduled-sync.log (rolled over to .1 past 1 MB).

Check on it with Get-ScheduledTaskInfo -TaskName AiChatExporterSync (look at LastTaskResult: 0 is success) and Get-Content cache\logs\scheduled-sync.log -Tail 50.

What to know before you rely on it

List only the providers that work unattended on that machine. The local providers (claude-code, codex) need no credentials and always work. The web providers depend on a session token that expires and can only be refreshed by hand via DevTools — so on a machine where that token is stale, scheduling them means a failed run every single day, which is a good way to learn to ignore failures you actually want to see. That is why --provider is repeatable in both installers: schedule the coding machine for claude-code + codex, the browser machine for chatgpt + claude.

Both installers pass --joplin-optional. If Joplin desktop isn't running, the sync warns instead of failing: the export has already captured the local transcripts (the part that can disappear), and the notes are rebuilt from the cache on the next run that finds Joplin up.

One provider failing does not skip the rest. Both installers loop over the providers in a single action rather than one action each, because systemd oneshot stops at the first failing ExecStart and Task Scheduler reports only the last action's result. Every provider is attempted; the run still exits non-zero if any failed. The loop is scheduling/run-sync.sh on Linux (the unit's ExecStart) and scheduling\run-sync.ps1 on Windows (the task's action).

Getting notified

A scheduled run is silent by default. Output goes to three pull-only places: the exporter's own log (cache/logs/exporter.log), the systemd journal on Linux (journalctl --user -u aichat-sync.service), and on Windows Task Scheduler's LastTaskResult plus cache\logs\scheduled-sync.log.

To have runs report back, set an ntfy topic in .env:

NTFY_TOPIC=my-archive-topic

Then check it works before waiting on a scheduled run:

ai-chat-exporter notify           # show settings
ai-chat-exporter notify --test    # send a test push

sync pushes a result whenever a topic is configured — a success carries the per-provider counts at low/default priority, a failure carries the reason at high priority with an alert tag, so a failed archive is distinguishable from a quiet one on your phone. NTFY_NOTIFY=failure notifies only on failure; off disables it; --notify / --no-notify override per run.

sync can only push from the end of a run it finished. A crash, an exit before the sync starts (the ToS gate, a cache error) or a launcher that can't build its venv sends nothing — and since each provider pushes separately, the ones that succeeded still say "OK", so a dead provider looks like a quiet day. The scheduler wrappers (scheduling/run-sync.sh, scheduling\run-sync.ps1) close that gap: any run that exits non-zero without the app having reported it gets a high-priority FAILED push naming the provider and, for a crash, the exception's class (claude-code: crashed (RecursionError)) — the class only, never its message, which can carry a conversation title. The traceback is in the journal on Linux and in cache\logs\scheduled-sync.log on Windows.

The message includes the machine name, which matters because both machines archive into one topic. It contains counts only — never conversation titles. A topic on public ntfy.sh is readable by anyone who knows its name, so if you want it private, self-host (NTFY_SERVER) or use an access-controlled topic with NTFY_TOKEN.

A notification is never fatal: if ntfy is unreachable, the run logs a warning and still reports its real exit code.

Acknowledge the ToS notice once, interactively. It's stored in the cache manifest per machine. Until then a scheduled run exits 1 with an explanation rather than hanging on a prompt no one can answer.