You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Sep 2, 2026. It is now read-only.
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).
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 »).
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 sync — relais 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 devices — appairage 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 conflits — LWW 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és — E2E 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.
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
auth.emdash.sh) +instanceIdtélémetry (device-ish, fragile).emdash4.dbdans userData) ; UUID PKs partout,updatedAt/createdAtquasi partout, colonnes JSON versionnées → base sync-friendly.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 dursrc/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 »).app_secretschiffrés safeStorage, liés au keychain de la machine → jamais syncables (l'app le sait déjà : la migration emdash3→4 les supprime).projects.path(unique),workspaces.path, PKeditor_buffers,ssh_connections.privateKeyPath→ remapping obligatoire.~/.claude/projects,~/.codex/sessions...) ;conversations.sessionIdpointe vers un état machine-specific → non resumable sur l'autre machine./grilling+/domain-modelingpour les tickets HITL ;/researchpour les tickets research ;/wayfinderpour piloter la carte.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 surupdatedAt) ; 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.dben 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 : snapshotVACUUM INTOexporté 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_settingsclé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.pathnullable (migration manuelle — le runner aforeign_keys=ONen 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.providerConfigscorrigé en machine-local (corrige Périmètre table-par-table de la sync #112). Automationsrepository-instance→ workspace-config v3 (workspaceId optionnel), résolution au run dans prepareCreateTask (échec avant commit), NULLprojects.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 sync — relais maison 2 endpoints (
/pullcursor +/pushLWW) + 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 > lastPushedsur 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 devices — appairage device-à-device zéro-dépendance (pas le compte emdash/upstream). Modèle :
spaces+tokens(sha256 en D1, scope espace,revoked_atretenu) ; device-id dédié (namespace KV machine-local, pas l'instanceId télémetry) ; token client 32 octets dansapp_secrets(safeStorage) ; secret d'appairage 32 octets copiable +emdash://join, mono-usage, TTL 15 min, budget de tentatives dans le Worker ; endpointsspace/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 conflits — LWW 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_tsms — affine Choix du mécanisme de sync #114) ; tombstones = version normale avec flagdeleted(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.boardRankexclu du payload (dérivé machine-local) ;project_remotescorrigé : 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és — E2E acté (relais opaque). Secret d'espace à deux moitiés dérivées :
join_half(seule transite, sous hash) etK0(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énementsync:status+syncController+ namespacesync:. 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