feat: 個人の記録を複数端末で同期できるようにする - #399
Merged
Merged
Conversation
競合するものとしないものを分けて扱う。 記録の実体は2種類ある。1ファイル1書き手のもの (Claude Code の projects/<uuid>.jsonl。セッションごとに別ファイルで、書くのはそれを走らせた 端末だけ、追記のみ) は、そのまま同期しても衝突しない。1ファイル複数書き手の もの (Claude の history.jsonl、nvim の keystrokes.jsonl) は、どの端末も同じ パスに追記するので必ず衝突する。 後者を素直に同期する方法は無いので、同期するのは元ファイルではなく personal-history/<ホスト名>/ 以下の写しにする。各端末は自分の名前の ディレクトリしか書かないので、構造として競合が起きない。ActivityWatch の aw-sync と同じ考え方。読む側は glob で拾えば全端末ぶんが1列になる。 atuin だけは例外で、本体の同期機構を使う。レコード単位の追記型で、しかも クライアント側で暗号化してから送るので、ファイルの写しを配るより筋がいい。 services.atuin を homeserver に立てる (database.createLocally が true なので、 この箱に初めてネイティブの PostgreSQL が1つ生える)。 副次的に、Claude の history.jsonl がどこにもバックアップされていなかった問題も 片付く。~/Sync/syncthing は restic の対象なので、書き出した時点で乗る。 実機で確認済み: - keystats (3.9MB、WAL 3.3MB) と activitywatch (250MB) の .backup が一貫した 写しを作る (event 1,152,774 件が読める) - history.jsonl のコピーが 12,777 行取れる - rsync のフィルタが .jsonl だけを拾う (587 ファイル、1.2GB) - homeConfigurations に personal-history-snapshot エージェントが入る
atuin の sync_address を公式 SaaS (api.atuin.sh) から自前サーバーへ。 同期の中身はもともとクライアント側で暗号化されているので、移す動機は プライバシーではなく他人のサーバーに依存しないこと。tailnet 内なので、 外出先で Tailscale が切れている間は同期だけが止まる (ローカルの記録と検索は 影響を受けない)。 母艦の Syncthing 側にも personal-history フォルダを宣言して、GUI での受け入れを 不要にする。**override を両方 false にしてある。** 既定は true で、その場合 ここに書いたものが唯一の正になり、GUI で足してある synchub と iphone が消える。 この Mac の syncthing 設定は意図的に宣言化されておらず既存を再利用している (darwin-services.nix の元コメント) ので、そこを壊さないため。 eval で overrideDevices/overrideFolders が false、宣言した folder が personal-history だけ、sync_address が自前サーバーになることを確認済み。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
「Claude のセッションも複数端末で同期したい。コンフリクト起きるかな?」に対する答えです。
起きるものと起きないものがあって、扱いを分ける必要がありました。
競合の分類
記録の実体は2種類に分かれます。
projects/<uuid>.jsonlhistory.jsonl、nvim のkeystrokes.jsonlsync-conflict-*が生える後者を素直に同期する方法はありません。追記位置が競合するので、マージも自動ではできません。
なので同期するのは元ファイルではなく写しにします。
personal-history/<ホスト名>/を掘って、各端末は自分の名前のディレクトリしか書かない。こうすると構造として競合が起きません。ActivityWatch のaw-syncが採っているのと同じ考え方です(あちらも「自分が作ったバケットしか書かない」という規約で競合解決を回避しています)。読む側は
personal-history/*/claude/history.jsonlのようにグロブで拾えば、全端末ぶんが1つの列になります。HPI のget_filesがグロブを受けるので、config.pyの書き換えは1回で済み、端末が増えても触りません。atuin だけ別扱い
シェル履歴はファイル同期しません。atuin 本体に同期機構があり、レコード単位の追記型で、しかもクライアント側で暗号化してから送ります。SQLite の写しを配るより明確に筋がいいです。
services.atuinを homeserver に立てます(shell.gapul.net、tailnet 内のみ)。注意点として
database.createLocallyが既定 true なので、この箱に初めてネイティブの PostgreSQL が1つ生えます(今までの postgres は全部コンテナ側でした)。UNIX ソケット越しなのでポートは開きません。openRegistrationは false のままにしてあります。アカウントを作るときだけ一時的に true にしてatuin registerし、済んだら戻す運用です。開けっ放しだと tailnet 内の誰でもアカウントを作れます。副産物: バックアップの穴が埋まる
前に指摘した「Claude の
history.jsonlがどこにもバックアップされていない」問題が、これで片付きます。~/Sync/syncthingは restic の対象なので、書き出した時点でバックアップにも乗ります。12,777 件の「自分が何を考えて何を頼んだか」は再現できない記録なので、これは効きます。検証(実機)
keystats.db(3.9MB、WAL 3.3MB)と ActivityWatch(250MB)の.backupが一貫した写しを作る。写した側から event 1,152,774 件が読めるhistory.jsonlのコピーが 12,777 行取れる.jsonlだけを拾う(587ファイル、1.2GB)homeConfigurations.gapulにpersonal-history-snapshotエージェントが入るサイズについて
Claude のセッション本体が 1.2GB あります。初回の同期はそれなりに重いです。以降は追記のみなので rsync の差分は小さくなります。
iPhone には配っていません。Syncthing のフォルダは母艦とだけ共有しています。
デプロイ後の手作業
openRegistrationを一時的に true にしatuin register、済んだら戻すatuin loginしてatuin syncpersonal-historyフォルダを母艦側でも受け入れるやっていないこと
nvim の
keystrokes.jsonlは同期対象に入れましたが、時刻がvim.uv.hrtime()の単調時計(起動からの経過ナノ秒)なので、他のソースとの時系列突き合わせには使えません。集めておく価値はありますが、繋ぐには上流に実時刻を足してもらう必要があります。