Memories / ドキュメント

このページの内容

クラウドストレージと同期の設計

状況: 2026-09-17 時点でステップ 1-4 は実装済みです。ステップ 5 には再現可能なベースラインがあります。 永続的な outbox/inbox の受領記録、変更マーカー、プロセス内の同期セッション、および MemoryTransport による障害注入を備えたプロバイダー非依存の実行機構が crates/core/src/sync.rs に実装されています。OneDrive/Google Drive のファイルアダプター、トークン更新、CLI の cloud sync は実装済みで、ローカルフィクスチャでテストされています。実際のプロバイダーアカウントは検証されていません。モバイルの AppAuth ログインとフォアグラウンド同期の操作は実装済みです。ネイティブの同意にはプラットフォームの登録が必要です。 現在の認可範囲については cloud-authorization.md を参照してください。この文書は OneDrive/Google Drive の要望を拡張し、同時に動作するデバイスが互いのデータベースファイルを破損したりロックしたりしてはならないという要件を追加します。

決定

SQLite をデバイスローカルのトランザクションストアとして維持します。クラウド API を通じて、不変で認証済みの暗号化オブジェクトを交換するプロバイダー非依存の同期プロトコルを追加します。各デバイスは独自のデータベース、識別情報、パスワード、ローカル書き込み主体を持ちます。クラウドストレージは信頼できないオブジェクト転送手段であり、マウントされたデータベースではありません。

稼働中の memories.db、ジャーナル、WAL、SHM、ロックファイルをクラウド管理の同期ディレクトリに置いてはいけません。リモートファイルシステム経由でデータベースを開かないでください。既定の OS アプリケーションデータの場所を維持します。既知の OneDrive/Drive ルートの検出と拒否は、将来の構成上の安全策であり、現在は強制されていません。任意の同期クライアントやマウントポイントを確実に検出することはできません。

flowchart LR
    A[Device A local SQLite] --> QA[Durable outgoing queue]
    QA --> C[OneDrive or Google Drive encrypted objects]
    C --> VB[Download and verify]
    VB --> B[Device B local SQLite]
    B --> QB[Durable outgoing queue]
    QB --> C
    C --> VA[Download and verify]
    VA --> A

既存の動作と制限

  • crates/core/src/store.rs: OS の排他ロックが、2 番目のローカルストア所有者を拒否します。SQLite は journal_mode=DELETE、synchronous=FULL、および暗号化された状態と署名付きチェックポイントのトランザクション更新を使用します。これは協調するローカルプロセスを保護しますが、独立したファイル複製間の分散ロックではありません。
  • 保存のたびに完全な State をシリアル化して暗号化します。ロック解除時には完全なチェックポイント履歴を確認します。ライブラリが大きくなると、書き込みコストと起動時間が増加します。これはスケーリング上の懸念であり、SQLite 自体を置き換える必要がある証拠ではありません。この評価についてワークロードのベンチマークは行っていません。
  • データベースの初期化では、同じディレクトリ内に非公開のステージングデータベースを構築する間、宛先の OS ロックを保持します。初期の暗号化された状態とチェックポイントをコミットし、SQLite を閉じ、ファイルを同期し、既存の宛先を置き換えずに公開します。公開前にクラッシュした場合も、最終パスは再試行に利用できます。クラッシュ後に放棄された .memories-init-* ファイルが残る可能性があります。それらを稼働中のデータベースとして読み込むことはなく、自動削除もしません。
  • Service は複製した状態で変更を処理し、受け入れる前にコミットします。Vault::merge は現在の vault を置き換える前に候補を検証します。署名付きリビジョン履歴は、既に同時編集と tombstone を保持しています。
  • デスクトップ IPC は最大 16 件の同時リクエストを受け付けます。状態のディスパッチはサービスの mutex を保持します。モバイルは個別のサービスロックと短いレジストリ検索を使用します。これらのクリティカルセクション内でクラウド HTTP リクエストを直接実行すると、ロックを含む他の操作が遅延します。これはブロッキングであり、循環デッドロックが実証されたわけではありません。クラウドワーカーはネットワーク I/O の前にサービスロックを解放する必要があります。デスクトップのリクエスト処理は現在、上限付きの同時接続を使用します。
  • 既存の同期エクスポートは、受信者向けに暗号化された vault 全体のエンベロープです。新たにクラウド認証を行ったデバイスでも、vault を復号またはインポートする前にデバイス承認が必要です。クラウドログインで暗号学的なデバイスのペアリングを置き換えてはいけません。

データベースの代替案

選択肢 このリポジトリへの適合性 判断
ローカル SQLite と不変のクラウドオブジェクト トランザクション、暗号化、監査、競合処理を再利用でき、サーバーは不要 推奨
ローカルストアとしての redb または LMDB 組み込みトランザクションストレージは利用可能だが、エンジンを変更してもデバイス間マージやクラウドロックは提供されない まだ移行を正当化できない
同期フォルダー内の 1 つの共有データベースまたは暗号化アーカイブ 独立したファイルアップロードでバージョンが上書きされる可能性があり、サイドカーロックとジャーナルは協調しない 稼働中の同期には不採用
PostgreSQL と専用同期 API サーバーの同時実行を集約できるが、運用サービスと独立したオフラインおよび暗号化プロトコルが必要 製品要件にサーバーが含まれる場合のみ検討
Automerge や Yjs などの CRDT ライブラリ 将来の共同テキスト編集には有用かもしれないが、永続ストレージや署名付き権限確認を置き換えない 認証情報やポリシー変更には導入しない

SQLite を WAL に切り替えることは、クラウド同期の解決策にはなりません。WAL は測定後にローカルの読み取り並行性を改善する可能性がありますが、同一ホストでの協調が必要であり、単一のローカル書き込み主体の必要性はなくなりません。

クラウドオブジェクトの契約

既存の vault 全体のエンベロープ形式から始め、ストレージ、暗号化、同期の粒度を同時に変更しないようにします。不変エンベロープの経路が実証された後にのみ、増分の暗号化されたリビジョンまたはチャンクを追加します。

  1. 各エンベロープを一度だけ生成し、アップロード前に正確なバイト列をローカル outbox に永続化します。暗号文オブジェクト全体の SHA-256 を論理 ID として使用します。再試行ではそのバイト列を再利用します。再試行ごとに再暗号化すると、新しいオブジェクトが作成されます。
  2. オブジェクトと公開記述子は不変です。共有の書き込み可能な latest.json、共有ロックファイル、グローバルなクラウドシーケンス、データベースの上書きはありません。将来導入する可変の head またはキャッシュは参考情報とし、オブジェクト一覧から復元可能にします。
  3. 最初にデータオブジェクトを公開し、次に形式バージョン、不透明な vault 参照、送信者、受信者、オブジェクトのハッシュと長さ、デバイスごとの先行要素とシーケンスを結び付ける不変の署名付き記述子を公開します。ファイル名には、平文のタイトル、アカウント名、パスワードハッシュではなく、暗号文識別子を使用します。
  4. クラウドの一覧取得と配信は遅延または順序変更が起こり得るため、記述子が依存オブジェクトより先に見えることがあります。すべてのオブジェクトが存在し検証されるまで、記述子を保留状態にします。依存オブジェクトが不足している間に部分的にインポートしたり、適用済み状態を進めたりしてはいけません。
  5. コミット前に、長さ、暗号文ハッシュ、署名、受信者、vault 範囲、現在の認可、既存の履歴の不変条件を検証します。プロバイダーの ETag とハッシュは転送メタデータであり、暗号学的な権威ではありません。
  6. 再送されたオブジェクトと重複するプロバイダーファイルは無害です。ファイル名ではなく、検証済みの論理オブジェクト ID で重複を排除します。クラウドファイルの欠落がアプリケーション上の削除を意味することはありません。削除は署名付き tombstone です。

クラウドプロバイダーはオブジェクトを削除または留保できます。ローカルで把握している head とチェックポイントにより一部のロールバックは検出できますが、信頼できる新しいチェックポイントを持たない新規デバイスは、最新状態を持つことを証明できません。この設計は完全性と明示的な競合を提供しますが、悪意あるプロバイダーに対する可用性や最新性は保証しません。

トランザクションとワーカーのライフサイクル

ローカルの書き込み主体は、アプリケーション状態、チェックポイント更新、outbox/inbox の状態、同期確認を所有します。ネットワーク処理の前後に 2 つの短いクリティカルセクションを使用します。

  1. ローカルで準備: 認可し、一貫したエクスポートを取得し、正確な暗号化アップロードジョブと世代またはセッションへの結び付けをコミットします。ユーザーの編集とその変更またはエクスポートマーカーは、クラッシュで作業が失われないようにアトミックにコミットする必要があります。
  2. ロックを解放して転送: 別のワーカーが、サービスの mutex やデータベースの書き込みトランザクションを保持せずに OAuth の更新、アップロード、ダウンロード、ページネーション、再試行を実行します。同時実行数、応答サイズ、再試行、接続と読み取りの期限に上限を設けます。プロバイダーのスロットリングと Retry-After に従います。
  3. ローカルでコミット: 書き込み主体を再取得し、認可とセッション世代を検証し、以前に取得したコピーではなく現在の状態にマージしてから、状態、チェックポイント、適用済み ID、inbox ステータスを一緒にコミットします。プロバイダーの一覧カーソルは、検出した作業が永続化された場合にのみ進められます。

ロックまたはログアウトはジョブをキャンセルし、遅れて届いた結果を無効にします。無効化されたセッションでは、新たな復号やインポートを完了できません。キャンセルしても、プロバイダーが既に受け入れた暗号文アップロードは取り戻せません。未完了のジョブは再開可能にします。アップロードの成功はクラウドへの永続化を意味し、別のデバイスによる受領確認ではありません。

OAuth トークンは暗号化されたデバイスローカルの状態を維持し、クラウドオブジェクト、デバイス承認パッケージ、エクスポートされたバックアップ、検索、MCP から除外します。リフレッシュトークンのローテーションは、その後の操作で新しいトークンを使用する前に永続化する必要があります。同時更新はアカウントごとに直列化し、HTTP 交換の間はサービスロックを保持しません。ワーカー用の公開トークン読み取り操作を追加してはいけません。

プロバイダー固有の動作

プロバイダー アプリケーションストレージ アップロードと再試行の要件
OneDrive Microsoft Graph のアプリケーションフォルダー、Files.ReadWrite.AppFolder 不変のオブジェクト名とアップロードセッションの競合動作 fail を使用する。既存オブジェクトがある場合は置き換えずにダウンロードして検証する。条件付きリクエストは、そのエンドポイントの文書にサポートが明記されている場合だけ使用する。
Google Drive appDataFolder、drive.appdata スコープ 成否が不明な作成を安全に再試行できるよう、ファイル ID を事前生成してジョブとともに永続化する。ファイル名を一意とみなさない。競合後に既存 ID と内容を検証する。

両アダプターは、ページネーション、再開可能な転送、上限付きのダウンロード、応答喪失からの復旧、プロバイダー固有のエラー処理を実装する必要があります。Drive がファイルシステムの名前変更やロック、または Graph と同じ事前条件の意味をサポートすると仮定しないでください。リダイレクトされたダウンロード URL と再開可能アップロード URL には、OAuth Bearer トークンを任意のホストへ送信しないための、明示的な認証情報転送ルールが必要です。

初期のアプリケーションフォルダー設計は、同じプロバイダーアカウントとアプリケーションに接続された、ユーザーのペアリング済みデバイスを対象とします。Google のアプリデータストレージは、異なるアカウント間でのファイル共有をサポートしません。ユーザー間の vault 共有には明示的な転送または共有の設計が必要です。OAuth スコープを暗黙に拡張してはいけません。両方を有効にした場合、OneDrive と Google Drive は独立したレプリカであり、相互同期されるフォルダーではありません。保留中と成功済みの転送を個別に追跡します。

実際のアカウントで検証する前に、OAuth クライアント登録とプロバイダーの同意構成が必要です。CLI の認可では、PKCE、state 検証、ループバックコールバックを伴うブラウザー OAuth を使用すべきです。モバイルには、サポートされるネイティブ認可フローとライフサイクル処理が必要です。デスクトップのコールバック前提を変更せずにモバイルへコピーしてはいけません。

競合、復旧、保持

  • 同時に作成された認証情報またはカードの両方のリビジョンを保持します。使用前に明示的な選択または解決を要求し、最新の実時間タイムスタンプを選んだり、パスワード文字列をマージしたりしません。既存のポリシーフォークと旧 epoch の隔離ルールを再利用します。
  • チェックサムまたは署名の失敗時は、受信オブジェクトを拒否し、以前のローカル状態を保持します。すべてをロックエラーとして報告せず、破損した入力、依存オブジェクトの不足、ログイン期限切れ、クォータ、内容の競合をステータスで区別します。バイト列がハッシュと一致しない応答は、バックオフ付きで再試行可能な状態を維持します。ハッシュは検証済みでも無効なオブジェクトには独立した拒否記録を設けます。それらが正常に適用された依存オブジェクトとして数えられることはありません。拒否がある場合は同期成功を報告できず、クラウドステータスにプロバイダーごとに表示され続けます。プロバイダーを切断すると、その拒否記録を消去し、後の接続で再走査できるようにします。拒否と成功したインポートが混在した旧形式の記録は一度だけ消去します。既存の署名付きパケットの受領記録は引き続き重複インポートを防止します。
  • ディレクトリの検出には 4096 エントリの inbox 予算と、依存オブジェクト用に予約された 16 スロットがあります。検証済みの記述子は、不足オブジェクトをハッシュで直接指定し、通常のダウンロードより先にスケジュールします。負荷が高い場合は、参照されていないエンベロープバッファーを破棄し、記述子が到着したときに再取得できます。部分的なディレクトリページでは、受け入れられなかった ID を越えてカーソルを進めません。
  • 状態をコミットするたびに、現在の vault アクセスが取り消された受信者へのキュー内エクスポートを削除します。ワーカーは、プロバイダー ID の割り当て後も含め、オブジェクト操作の間にセッションを検証します。Google のファイル ID は最初のアップロード前に暗号化された outbox に保存するため、確認応答の喪失や再起動後も、同じ不変のプロバイダーオブジェクトを再利用します。
  • 最初は古いクラウドオブジェクトを保持します。安全なガベージコレクションには、デバイスごとの受領確認、オフラインデバイスのポリシー、失効処理、復旧用の保持ポリシーが必要です。一覧からの欠落と経過時間だけでは不十分です。
  • 独立した暗号化バックアップを保持します。複数デバイスの同期は有効な削除も配布するため、ある時点への復旧の代わりにはなりません。
  • スキーマ移行の前に、復旧を考慮した移行経路を追加します。ローカルでロックし、検証済みのバックアップを保持し、トランザクションで移行し、検証してから有効化します。移行に失敗した場合は、以前のストアを使用可能なまま維持する必要があります。

実装の順序

  1. 完了。 上限付きの書き込みリクエスト(CloudSyncEnqueue / CloudSyncWork / CloudSyncAck)と、暗号化された State 内の永続的な outbox/inbox の受領記録。SQLite の暗号化、journal_mode=DELETE、状態全体の保存は変更していません。WAL への移行やデータベースエンジンの移行はありません。
  2. 完了、ローカルトランスポートのみ。 プロバイダー非依存の不変オブジェクト実行機構(sync::execute)と、MemoryTransport による障害注入。テストは正確なバイト列の再試行、コミット後の再オープン、重複 put の検証、データより先に届く記述子、破損オブジェクト、ロックまたはログアウトによる無効化、ペアリング済みの 2 デバイスを対象とします。このテスト用トランスポートは実際のクラウドアカウントを使用しません。
  3. 完了、ローカルフィクスチャのみ。 トークン更新、OneDrive/Google Drive の不変オブジェクトアダプター、デーモンの CloudSyncRun(書き込みロックの外で HTTP を実行)、および memories cloud sync。実際の Microsoft/Google アカウントは認可していません。
  4. 実装済み、実際のネイティブ同意は未完了。 モバイルログインは AppAuth を使用し、フォアグラウンド同期はコアの実行機構を共有します。バックグラウンド移行はすべてのハンドルをロックし、遅れて届いた結果をキャンセルします。ウィジェットとネイティブ FFI の回帰テストはキャンセルを対象とします。実際のプロバイダーと実機での同意は引き続きリリース条件です。バックグラウンドのスケジューリングは意図的に有効にしていません。
  5. ベースラインあり。 ライブラリサイズ、保存、ロック解除、検索の遅延、および同期通信量の使い捨て環境でのリリースベンチマークは、パフォーマンスを参照してください。必要な場合は、ローカルの状態全体の blob を、トランザクション受領記録を伴う vault、リビジョン、キーごとの暗号化行へ移行し、その後に増分クラウドオブジェクトを導入します。署名付き ID、リビジョン履歴、暗号文のプライバシー、バックアップ復旧を維持します。

検証の証拠と残る条件

Windows では、crates/core/tests/storage_resilience.rs が使い捨てストアとサブプロセスを検証します。この評価では 3 つのシナリオが成功しました。

  • 別のプロセスがストアを所有している間は、独立したプロセスが拒否されます。所有プロセスを強制終了すると OS ロックが解放され、元の暗号化された状態を再度開けます。
  • 子プロセスが SQLite の書き込みトランザクションを開始し、変更済みページをロールバックジャーナルへ強制的に書き出し、コミット前に強制終了されます。Store::unlock が元の状態とチェックポイントを検証し、PRAGMA integrity_check が ok を返します。
  • 認可済みの 2 デバイスが同じアセットを独立して編集します。どちらのマージ順でも収束し、リプレイは冪等で、両方の head が残ります。親が欠けた受信スナップショットは、正常な状態を変更せずに拒否されます。

意図的に無視されるヘルパーは、親テストが明示的に起動します。次を実行してください。

cargo test -p memories-core --test storage_resilience --locked -j 1 -- --test-threads=1
cargo test -p memories-core --lib store::tests --locked -j 1 -- --test-threads=1

store.rs の初期化回帰テストは、ステージング後かつ公開前にサブプロセスを強制終了し、最終ストアを再作成してロックを解除します。また、宛先のロックが保持されることと、既存または新たに現れた宛先を決して上書きしないことも検証します。OAuth テストは、ロック後に遅れて届いた結果の無効化を対象とします。

これらはローカルのプロセスクラッシュとモデルのテストであり、実際の停電、ディスクまたはコントローラーの障害、クラウドクライアントによるファイル置換、実際のプロバイダーの動作に対する証明ではありません。ディスク容量不足、大きなライブラリ、公開時の停電、クラウドの再試行と公開の境界は、まだテストが必要です。 この評価では、実際のクラウドアカウントを認可したり書き込んだりしていません。

参考資料

2026-09-09 に確認した公式文書: