From b0aa05a2b1e29867b13451bbfe1e5c99c06f0b81 Mon Sep 17 00:00:00 2001 From: JesseMarkowitz Date: Tue, 18 Aug 2026 10:31:28 -0400 Subject: [PATCH] fix sync bug for scheduled work --- FUTURE.md | 33 ++++++++++++++++++++++---- scheduling/Register-AiChatSyncTask.ps1 | 12 +++++++++- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/FUTURE.md b/FUTURE.md index 1353bd2..2e01252 100644 --- a/FUTURE.md +++ b/FUTURE.md @@ -329,19 +329,44 @@ machines — coding sessions (`claude-code`, `codex`) on the Linux box, web chat conversations from everywhere, instead of per-machine islands that only meet inside Joplin. +**Intended split of responsibility (decided 2026-08-18).** Each machine keeps +running the exporter locally and keeps doing what it is uniquely able to do — +read that machine's local transcripts, and hold the browser session for the web +providers. What changes is where the output goes: instead of syncing to Joplin +itself, a local run **uploads its conversations to the StartOS storage area**, +and the StartOS service owns the Joplin connection for the whole corpus. + +That inverts today's arrangement, where every machine talks to its own Joplin +desktop, and it removes two problems we already have: + +- **The Joplin-availability race disappears from the clients.** A scheduled run + currently has to find Joplin desktop open on that same machine — the + 2026-08-18 09:02 timer run exported fine and then skipped the sync because + Joplin did not start until 09:07. Uploading to a server that is always up has + no such window, and `--joplin-optional` stops being load-bearing. +- **One Joplin integration instead of N.** Notebook naming, resource upload and + note-update logic run once, server-side, against one manifest — rather than + each machine independently deciding what a notebook is called and racing to + update the same note. + What this would need, and what it would *not*: - **Not** a headless web-provider login. The hard sub-problem the original drop retired stays retired: the web providers can keep running interactively on the machine that has the browser, pushing their output to the server. Only the local providers need to run server-side, and they need no tokens at all. -- Machines would need to reach the server — a sync/push step, or the service - reading transcript directories exported from each machine. +- An upload step in the client — the counterpart of today's `joplin` command, + pointed at the StartOS service instead of a local Joplin API. Probably a + `--upload`/`push` alongside `sync`, so a scheduled client run stays one line. - Per-machine identity in the corpus, which the exporter currently does not track: `claude_code.resolve_roots` deliberately merges multiple roots with "no per-machine label". Centralizing would make that label load-bearing. -- Conflict handling for one conversation seen by two machines. The cache - manifest is per-machine today. +- Conflict handling for one conversation seen by two machines, and a decision + about whether the server or the client owns the cache manifest. It is + per-machine today, and that is what makes "already up to date" mean anything. +- A story for what the client keeps locally after a successful upload. Exports + are the only copy of `claude-code` / `codex` transcripts once Codex prunes its + rollouts, so the client should probably keep them rather than move them. Meanwhile Joplin is sufficient — it already syncs (encrypted) offsite, and it is where the archive is actually read. This is a "nice eventually", not a gap. diff --git a/scheduling/Register-AiChatSyncTask.ps1 b/scheduling/Register-AiChatSyncTask.ps1 index 5074fbc..8883e8c 100644 --- a/scheduling/Register-AiChatSyncTask.ps1 +++ b/scheduling/Register-AiChatSyncTask.ps1 @@ -50,9 +50,15 @@ if (-not (Test-Path $launcher)) { # Task Scheduler runs multiple actions in order but reports only the last one's # result, so a failure in an earlier provider would be invisible. The loop keeps # going after a failure and propagates a non-zero exit code. +# +# /v:on and !RC! are required, not stylistic: cmd expands every %VAR% on a +# command line *before* running any of it, so "exit /b %RC%" would report the +# value RC had before the loop ever ran - i.e. always success. Delayed expansion +# reads it at the point of use. $loop = ($Provider | ForEach-Object { "`"$launcher`" sync --provider $_ --joplin-optional || set RC=1" }) -join ' & ' +$taskArgs = "/v:on /c set RC=0 & $loop & exit /b !RC!" $actions = New-ScheduledTaskAction -Execute 'cmd.exe' ` - -Argument "/c set RC=0 & $loop & exit /b %RC%" ` + -Argument $taskArgs ` -WorkingDirectory $repo $trigger = New-ScheduledTaskTrigger -Daily -At $Time @@ -75,6 +81,10 @@ Register-ScheduledTask -TaskName $TaskName ` Write-Host "Registered '$TaskName' - daily at $Time for: $($Provider -join ', ')" Write-Host '' +Write-Host 'Command the task will run:' +Write-Host " cmd.exe $taskArgs" +Write-Host " (working directory: $repo)" +Write-Host '' Write-Host 'Next steps:' Write-Host " * Run it once now: Start-ScheduledTask -TaskName $TaskName" Write-Host " * Check the result: Get-ScheduledTaskInfo -TaskName $TaskName"