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

125 lines
5.5 KiB
Markdown

# 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](../README.md) · [Providers and Tokens](providers.md) · [CLI Reference](cli.md) · [Troubleshooting](troubleshooting.md)
---
## 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.
```bash
./ai-chat-exporter sync # every configured provider
./ai-chat-exporter sync --provider codex # just one
```
### Linux (systemd user timer)
```bash
./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)
```powershell
.\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](https://ntfy.sh) topic in `.env`:
```bash
NTFY_TOPIC=my-archive-topic
```
Then check it works before waiting on a scheduled run:
```bash
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.