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