Skip to content
This repository was archived by the owner on Sep 2, 2026. It is now read-only.
This repository was archived by the owner on Sep 2, 2026. It is now read-only.

Carte Wayfinder — Sync multi-machine emdash #109

Description

@64ix

Destination

Un spec prête à être implémentée : la sync multi-machine d'emdash (macOS + Windows), usage perso mono-utilisateur, auto quasi-continue quand l'app tourne + sync au lancement + bouton manuel. Périmètre : tasks/états, projects, settings portables, métadonnées de conversations. Hors périmètre : worktrees/repos (git est déjà la sync), credentials (safeStorage machine-bound), transcripts de conversations (non stockés par l'app).

Notes

  • Usage perso mono-utilisateur ; identité existante : compte emdash (GitHub OAuth, auth.emdash.sh) + instanceId télémetry (device-ish, fragile).
  • DB : SQLite via Drizzle (emdash4.db dans userData) ; UUID PKs partout, updatedAt/createdAt quasi partout, colonnes JSON versionnées → base sync-friendly.
  • Mécanisme (décision Q4) : relais row-level maison sur Cloudflare Workers + D1 free, opéré par le fork owner ; produits de sync écartés par des faits (Turso : pas de sync sélective + FTS5 ; PowerSync/Electric : Postgres ; file-sync : Recherche : sync de fichier SQLite — viabilité #111).
  • Fork-only : la feature n'existera que sur 64ix/emdash. Le backend de sync est indépendant d'upstream (comptes Turso/CF D1/VPS contrôlés par le fork owner). En revanche le compte emdash (auth.emdash.sh, URL en dur src/main/core/account/config.ts:3) est l'infra d'auth upstream — dépendre de lui pour l'identité = dépendre d'upstream ; l'appairage device-à-device est l'alternative zéro-dépendance (critère à trancher dans « Identité et appairage des devices »).
  • UX cible (décision utilisateur Q5) : auto quasi-continue + sync au lancement + bouton manuel.
  • Secrets : app_secrets chiffrés safeStorage, liés au keychain de la machine → jamais syncables (l'app le sait déjà : la migration emdash3→4 les supprime).
  • Chemins absolus dans le DB : projects.path (unique), workspaces.path, PK editor_buffers, ssh_connections.privateKeyPath → remapping obligatoire.
  • L'historique chat-ui n'est PAS stocké par l'app : transcript en mémoire / dans les stores des providers (~/.claude/projects, ~/.codex/sessions...) ; conversations.sessionId pointe vers un état machine-specific → non resumable sur l'autre machine.
  • Skills : /grilling + /domain-modeling pour les tickets HITL ; /research pour les tickets research ; /wayfinder pour piloter la carte.
  • Effort : produit un spec — le dernier ticket assemble les décisions en spec (override « plan don't do »).
  • Tracker : issues GitHub sur 64ix/emdash (jamais upstream). Conventions : sous-issues natives + dépendances natives, voir docs/agents/issue-tracker.md.

Decisions so far

  • Recherche : paysage des mécanismes de sync — fiche agents/research/paysage-sync.md : Turso/libSQL seul à sync un vrai SQLite Drizzle-compatible (local-first, offline, 0€) ; backend Workers+D1 minimal (0€, pattern auth.emdash.sh, 2 endpoints sur updatedAt) ; Syncthing (continu E2E, mais subordonné au verdict file-sync) ; PowerSync (complet, force Postgres + re-archi) ; Replicache/Electric écartés mais leur design push-pull-cursor fait référence pour un serveur maison. Décision en aval dans « Choix du mécanisme de sync ».

  • Recherche : sync de fichier SQLite — viabilitéemdash4.db en WAL + busy_timeout=5000, connexion RW ouverte toute la vie de l'app : file-sync du DB vivant = non sûr (corruption mid-transaction documentée). Patterns sûrs : snapshot VACUUM INTO exporté vers un dossier syncé, importé au démarrage ; un sync chaud bidirectionnel exige un oplog applicatif (style Litestream). File-sync disqualifié comme mécanisme principal pour l'UX « auto quasi-continue ».

  • Périmètre table-par-table de la sync — IN : projects (path = colonne locale par machine), project_settings, project_remotes (ligne entière, pas d'updatedAt), tasks (workspaceId dangle OK), conversations (métadonnées ; exclus sessionId/agentStatus/agentStatusSeen), automations (enabled local par machine, défaut désactivée à l'import), kv:prompt-library, app_settings clés portables (exclus localProject/customSoundPath/defaultShell/providerConfigs). OUT : automation_runs, pull_requests + 4 dérivées, editor_buffers, terminals, workspaces, kv (sauf prompt-library ; account déféré à Identité et appairage des devices #115), provider_accounts, ssh_connections, messages, app_secrets. Glossaire Projet vs Workspace dans CONTEXT.md. Conséquences spec : réparation workspace manquant, ré-attachement path projet, affordance automation importée (notées dans UX du sync : cadence, première sync, conflits #118 et Rédiger le spec multi-machine sync #119).

  • Remapping des chemins absolus macOS ↔ Windows — modèle « non attaché » : projects.path nullable (migration manuelle — le runner a foreign_keys=ON en transaction, un rebuild naïf cascade-wiperait les enfants) + état de première classe distinct de path-not-found. Path local jamais envoyé ; path SSH voyage (hypothèse même-hôte, ré-attach en secours) ; fusion ssh par fingerprint (host,port,user)+path. project_settings: worktreeDirectory+workspaceProvider machine-locaux. providerConfigs corrigé en machine-local (corrige Périmètre table-par-table de la sync #112). Automations repository-instance → workspace-config v3 (workspaceId optionnel), résolution au run dans prepareCreateTask (échec avant commit), NULL projects.repositoryWorkspaceId à l'import. Fusion typée par (remoteName, url normalisée) des remotes live ; ambiguïté local+ssh → demander ; RPC d'attachement dédié ; auto-attach silencieux à l'import. Colonnes mortes (workspaceProviderData/workspaceIntent) jamais envoyées. Validation par 2 sous-agents.

  • Choix du mécanisme de syncrelais maison 2 endpoints (/pull cursor + /push LWW) + long-poll, hébergé CF Workers + D1 free (décision utilisateur), opéré par le fork owner. Serveur = KV générique (table, pk, row JSON opaque, updated_at, deleted) — ne parse rien → tolère le version skew. Push client = updatedAt > lastPushed sur les tables autorisées (pas de hook write-path) ; pull avec garde LWW ; tombstones pour les deletes durs. Auth = Identité et appairage des devices #115 (tokens device + D1) ; E2EE = Chiffrement E2E et clés #117 (payload chiffré client). Turso/PowerSync/Electric/file-sync écartés par des faits (cf. résolution). Brouillard de la carte : vide — version skew, Windows, première sync et métadonnées de conversation gradués (résolution Choix du mécanisme de sync #114 ; UX dans UX du sync : cadence, première sync, conflits #118, exigences dans Rédiger le spec multi-machine sync #119).

  • Identité et appairage des devicesappairage device-à-device zéro-dépendance (pas le compte emdash/upstream). Modèle : spaces + tokens (sha256 en D1, scope espace, revoked_at retenu) ; device-id dédié (namespace KV machine-local, pas l'instanceId télémetry) ; token client 32 octets dans app_secrets (safeStorage) ; secret d'appairage 32 octets copiable + emdash://join, mono-usage, TTL 15 min, budget de tentatives dans le Worker ; endpoints space/join/devices/revoke. Le secret double comme seed E2E (HKDF salt=space_id, Chiffrement E2E et clés #117) — perte du dernier device = perte des données (pas d'escrow, étape « sauvegarder le secret »). UI = écran Devices (patron SshConnectionsSettingsCard). UX d'appairage → UX du sync : cadence, première sync, conflits #118, détails → Rédiger le spec multi-machine sync #119.

  • Modèle de conflitsLWW ligne entière, silencieux, versions serveur autoritatives par espace (estampillées transactionnellement, pattern Replicache ; jamais de timestamps clients). Clock de sync locale par trigger SQLite (sync_ts ms — affine Choix du mécanisme de sync #114) ; tombstones = version normale avec flag deleted (delete bat l'édition plus vieille, édition plus récente ressuscite ; GC version-min + cap 90 j) ; push-then-pull (rows sales jamais écrasées avant push) ; jamais rejeter un push stale. boardRank exclu du payload (dérivé machine-local) ; project_remotes corrigé : sync initiale seule (cache machine-dérivé, guerre d'écritures sinon — correction de Périmètre table-par-table de la sync #112) ; JSON versionnés transportés en raw (jamais round-trip d'un blob future-version) ; enveloppe E2E = métadonnées en clair + corps chiffré (Chiffrement E2E et clés #117).

  • Chiffrement E2E et clésE2E acté (relais opaque). Secret d'espace à deux moitiés dérivées : join_half (seule transite, sous hash) et K0 (ne transite jamais, stockée safeStorage/app_secrets). Ajout de device = secret [join_frais ‖ K0 constante] ; clés publiques par device écartées (canal humain déjà présent). AES-256-GCM, clé dérivée par ligne (HKDF salt=row_id) + nonce aléatoire 96 bits, enveloppe versionnée {alg,key_id,nonce,ct}, AAD = table‖pk‖version‖key_id. Rekey = op admin rare (re-chiffrement batch). Révocation token = accès seulement, pas confidentialité. TLS requis. Détails → Rédiger le spec multi-machine sync #119.

  • UX du sync : cadence, première sync, conflits — widget SidebarFooter (pattern provider-usage) : icône d'état (synchro/à jour/hors-ligne pending/erreur) + bouton « Sync now » toujours visible + popover (dernière sync, erreurs). Conversations sans transcript = affichées avec état « non resumable ». Long-poll temps réel + sync au lancement + bouton manuel. Hors-ligne : écritures locales + badge pending + reconnexion auto. Onboarding B : « Rejoindre un espace (coller le secret) » au premier plan. Projets non attachés : auto-attach (Remapping des chemins absolus macOS ↔ Windows #113) sinon badge + action Attacher. Automations importées : badge « importée, désactivée » (+ champ source). Backend : événement sync:status + syncController + namespace sync:. Arrière-plan : quitte sur Win/Linux (couvert par sync au lancement), tourne sur macOS.

  • Rédiger le spec multi-machine sync — spec publié : #130 « [Spec] Sync multi-machine emdash » (ready-for-agent). Assemble toutes les décisions : relais CF Workers+D1, périmètre (Périmètre table-par-table de la sync #112 amendé), remapping/unattached (Remapping des chemins absolus macOS ↔ Windows #113), appairage device-à-device (Identité et appairage des devices #115), LWW + tombstones + triggers sync_ts (Modèle de conflits #116), E2E AES-GCM (Chiffrement E2E et clés #117), UX (UX du sync : cadence, première sync, conflits #118). Seam de test = SyncEngine (RelayTransport injectable). La carte atteint sa destination — brouillard vide, aucune décision en suspens.

Not yet specified

Out of scope

Emdash-Task: b653f3b3-110e-4ed5-8e19-ef2fe1039f20

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions