# Phase 2: Daemon et i3 Bridge - Research
**Researched:** 2026-03-23
**Domain:** Go daemon (Unix socket IPC) + i3 IPC + X11 PID resolution
**Confidence:** HIGH
## Summary
La Phase 2 transforme vmux d'un CLI one-shot en un daemon persistant (vmuxd) qui maintient un registre des sessions a jour. Le daemon communique avec le CLI via un Unix socket JSON. L'integration i3 permet de mapper chaque session Claude Code a son workspace et de switcher en une action.
**Decouverte critique :** i3 GetTree() ne contient PAS de champ PID dans ses nodes. Le contexte (D-04) suppose que "i3 GetTree() donne le PID de chaque fenetre terminal", ce qui est faux. Le champ `Window` (X11 window ID) est present, et il faut lire `_NET_WM_PID` via X11 pour obtenir le PID du terminal. La bonne nouvelle : `go.i3wm.org/i3/v4` depend deja de `BurntSushi/xgbutil` qui expose `ewmh.WmPidGet()`. Aucune dependance supplementaire.
L'algorithme de mapping est : i3 GetTree() -> Node.Window -> `ewmh.WmPidGet()` -> terminal PID -> walk up /proc PPID chain depuis chaque Claude PID jusqu'a trouver un terminal PID connu.
**Recommandation principale :** Utiliser `go.i3wm.org/i3/v4` + `BurntSushi/xgbutil/ewmh` (deja en transitive dep) pour le mapping PID-workspace. Pas de shell-out vers xdotool/xprop.
## User Constraints (from CONTEXT.md)
### Locked Decisions
- **D-01:** vmuxd communique avec le CLI via un Unix socket (`~/.vmux/vmux.sock`). Protocole JSON sur le socket.
- **D-02:** vmuxd poll /proc + JSONL toutes les 5 secondes pour mettre a jour le registre des sessions. La Phase 3 (hooks) reduira la latence a < 1s.
- **D-03:** Autostart : `vmux list` (ou tout autre commande) demarre vmuxd automatiquement s'il ne tourne pas. `vmux stop` pour arreter proprement.
- **D-04:** Mapping session -> workspace via le PID. i3 GetTree() donne le PID de chaque fenetre terminal ; on croise avec le PID du processus Claude Code pour trouver le workspace. **NOTE RECHERCHE : i3 GetTree() n'a PAS de champ PID. Utiliser Node.Window + _NET_WM_PID via X11. Meme resultat final.**
- **D-05:** Si plusieurs sessions Claude Code sont dans le meme workspace i3, elles sont toutes listees avec le meme workspace.
- **D-06:** Utiliser `go.i3wm.org/i3/v4` (lib officielle Go pour i3 IPC). Premiere dependance externe du projet. API : GetTree, GetWorkspaces, RunCommand.
- **D-07:** `vmux switch` utilise du fuzzy match : `vmux switch auth` matche la premiere session contenant "auth" dans branche git, label ou cwd.
- **D-08:** `vmux label "texte"` attribue un label humain. Le label est stocke en memoire dans vmuxd (persiste sur disque dans `~/.vmux/labels.json` pour survivre aux redemarrages).
- **D-09:** Le daemon track les transitions d'etat. Quand une session passe a NeedsInput, il enregistre le timestamp. `vmux list` affiche "depuis X min".
### Claude's Discretion
- Format de communication JSON sur le Unix socket (structure des requetes/reponses)
- Gestion du PID file pour le daemon
- Gestion des erreurs de connexion i3 IPC (fallback gracieux si i3 non disponible)
### Deferred Ideas (OUT OF SCOPE)
None
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| DISC-04 | vmux permet d'associer un label humain a une session | Labels stockes en memoire + persistes dans `~/.vmux/labels.json`. Protocole socket : requete `label` avec session_id + texte. |
| I3-01 | vmux mappe chaque session a son workspace i3 | Algorithme : i3 GetTree -> Node.Window -> ewmh.WmPidGet -> terminal PID -> PPID chain matching. Verifie sur machine : 6 Claude PIDs correctement mappes a leurs workspaces. |
| I3-02 | vmux permet de switcher vers le workspace i3 d'une session en une action | `i3.RunCommand("workspace number N")` via go.i3wm.org/i3/v4. Fuzzy match sur label/branche/cwd pour trouver la session. |
| STATE-04 | vmux affiche le temps ecoule depuis que la session attend | Daemon track les transitions d'etat avec timestamp. Quand state passe a NeedsInput, enregistre `WaitingSince`. Affichage "depuis X min" calcule par le CLI. |
## Project Constraints (from CLAUDE.md)
- NixOS : dependances via `nix-shell`
- Code simple, expressif, bien concu
- Commentaires uniquement pour le "Pourquoi"
- TDD Chicago School
- Ne jamais modifier les .env
- Francais avec accents dans les communications
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| Go | 1.25.7 | Langage | Deja utilise en Phase 1. Disponible via `nix-shell -p go`. |
| go.i3wm.org/i3/v4 | v4.24.0 | i3 IPC (GetTree, GetWorkspaces, RunCommand) | Lib officielle Go pour i3. Couvre 100% du protocole IPC. Retry transparent sur restart i3. |
| BurntSushi/xgbutil/ewmh | v0.0.0-20190907113008 | Lecture _NET_WM_PID des fenetres X11 | Dependance transitive de go.i3wm.org/i3/v4. Pas d'ajout explicite. `ewmh.WmPidGet(xu, win)` retourne le PID. |
| Go stdlib (net, encoding/json, os, sync) | - | Unix socket, JSON protocol, /proc, concurrence | Daemon et IPC ne necessitent aucune lib externe. |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| BurntSushi/xgb | v0.0.0-20210121224620 | Connexion X11 bas niveau | Dependance transitive. Necessaire pour initialiser xgbutil. |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| BurntSushi/xgbutil pour _NET_WM_PID | exec xdotool | 37ms pour 14 fenetres, acceptable mais shell-out inutile puisque xgbutil est deja en dep transitive |
| stdlib net (Unix socket) | gRPC / protobuf | Over-engineered pour un daemon local avec 4 requetes |
| stdlib flag avec sous-commandes manuelles | cobra | Over-engineered, 4 sous-commandes max |
| PID file maison | github.com/nightlyone/lockfile | Trop simple pour justifier une dep (un os.WriteFile + flock) |
**Installation:**
```bash
nix-shell -p go --run "go get go.i3wm.org/i3/v4@v4.24.0"
```
**Version verification:** `go.i3wm.org/i3/v4 v4.24.0` correspond a i3 4.24 installe sur la machine. `BurntSushi/xgb` et `xgbutil` sont des dependances transitives, pas d'ajout explicite dans go.mod.
## Architecture Patterns
### Recommended Project Structure
```
vmux/
├── main.go # Point d'entree CLI, dispatch sous-commandes, autostart daemon
├── daemon.go # vmuxd : boucle de poll, registre en memoire, Unix socket server
├── daemon_test.go
├── client.go # Client Unix socket (CLI -> daemon)
├── client_test.go
├── protocol.go # Types requete/reponse JSON du socket
├── protocol_test.go
├── i3bridge.go # Integration i3 : GetTree, PID resolution, RunCommand
├── i3bridge_test.go
├── workspace.go # Mapping PID -> workspace via PPID chain + _NET_WM_PID
├── workspace_test.go
├── proc.go # (Phase 1, inchange)
├── proc_test.go
├── session.go # (Phase 1, inchange)
├── session_test.go
├── state.go # (Phase 1, inchange)
├── state_test.go
├── display.go # Etendu : workspace, label, temps d'attente
├── display_test.go
├── types.go # Enrichi : Workspace, Label, WaitingSince dans Session
├── go.mod
├── go.sum
└── shell.nix
```
### Pattern 1: Daemon avec Unix Socket
**What:** vmuxd ecoute sur `~/.vmux/vmux.sock`, recoit des requetes JSON, repond avec le registre.
**When to use:** Toutes les commandes CLI (`list`, `switch`, `label`, `stop`).
```go
// Protocole JSON simple : une requete = un objet JSON, une reponse = un objet JSON
// Delimiter : chaque message termine par \n (JSON Lines sur le socket)
type Request struct {
Action string `json:"action"` // "list", "switch", "label", "stop"
Args json.RawMessage `json:"args,omitempty"`
}
type Response struct {
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
Sessions []SessionInfo `json:"sessions,omitempty"`
}
type SessionInfo struct {
PID int `json:"pid"`
SessionID string `json:"session_id"`
Cwd string `json:"cwd"`
GitBranch string `json:"git_branch"`
State string `json:"state"`
Preview string `json:"preview"`
Workspace string `json:"workspace"`
Label string `json:"label,omitempty"`
WaitingSince *time.Time `json:"waiting_since,omitempty"`
}
```
### Pattern 2: Autostart Daemon
**What:** Le CLI tente de se connecter au socket. Si echec, il lance vmuxd en arriere-plan puis re-essaie.
**When to use:** A chaque commande CLI.
```go
func ensureDaemon(sockPath string) error {
conn, err := net.DialTimeout("unix", sockPath, 500*time.Millisecond)
if err == nil {
conn.Close()
return nil // daemon deja actif
}
// Lancer le daemon en background
exe, _ := os.Executable()
cmd := exec.Command(exe, "daemon")
cmd.Stdout = nil
cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} // detacher du terminal
if err := cmd.Start(); err != nil {
return fmt.Errorf("cannot start daemon: %w", err)
}
// Attendre que le socket soit pret (retry avec backoff)
for i := 0; i < 20; i++ {
time.Sleep(50 * time.Millisecond)
conn, err := net.DialTimeout("unix", sockPath, 200*time.Millisecond)
if err == nil {
conn.Close()
return nil
}
}
return fmt.Errorf("daemon did not start in time")
}
```
### Pattern 3: PID -> Workspace Resolution (CORRIGE vs D-04)
**What:** Mapper un PID Claude Code a son workspace i3.
**Algorithme verifie sur la machine :**
```
1. i3.GetTree() -> arbre de nodes
2. Pour chaque node avec Window > 0 :
a. ewmh.WmPidGet(xu, xproto.Window(node.Window)) -> terminal PID
b. Stocker dans map[terminalPID]workspaceName
3. Pour chaque Claude PID :
a. Lire /proc/PID/status -> PPid
b. Remonter la chaine PPID jusqu'a trouver un terminalPID dans la map
c. Le workspace correspondant est le workspace de la session
```
**Verification reelle sur la machine :**
| Claude PID | Terminal PID (ancestor) | Workspace |
|------------|------------------------|-----------|
| 10466 | 10138 | 1 |
| 13736 | 11864 | 10 |
| 148021 | 147724 | 2 |
| 16030 | 15281 | 10 |
| 179544 | 178345 | 3 |
| 87142 | 86863 | 3 |
Chaine typique : `claude -> zsh -> .sakura-wrapped -> i3`
```go
func resolveWorkspace(claudePID int, terminalWorkspaces map[int]string) string {
pid := claudePID
for i := 0; i < 20; i++ { // max 20 ancestors (securite)
if ws, ok := terminalWorkspaces[pid]; ok {
return ws
}
ppid, err := readPPID(pid)
if err != nil || ppid <= 1 {
return "" // pas de workspace trouve
}
pid = ppid
}
return ""
}
func readPPID(pid int) (int, error) {
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid))
// Parse "PPid:\t" line
}
```
### Pattern 4: Workspace Switch via i3 IPC
**What:** Basculer vers le workspace d'une session.
```go
func switchToWorkspace(wsName string) error {
results, err := i3.RunCommand(fmt.Sprintf("workspace number %s", wsName))
if err != nil {
return err
}
if len(results) > 0 && !results[0].Success {
return fmt.Errorf("i3: %s", results[0].Error)
}
return nil
}
```
### Pattern 5: Fuzzy Match pour Switch
**What:** `vmux switch auth` matche la premiere session contenant "auth" dans label, branche, ou cwd.
**Ordre de priorite :** label > branche git > dernier segment du cwd.
```go
func fuzzyMatch(query string, sessions []SessionInfo) *SessionInfo {
query = strings.ToLower(query)
// Priorite 1: label contient query
for _, s := range sessions {
if strings.Contains(strings.ToLower(s.Label), query) {
return &s
}
}
// Priorite 2: branche git contient query
for _, s := range sessions {
if strings.Contains(strings.ToLower(s.GitBranch), query) {
return &s
}
}
// Priorite 3: cwd contient query
for _, s := range sessions {
if strings.Contains(strings.ToLower(s.Cwd), query) {
return &s
}
}
return nil
}
```
### Pattern 6: Labels Persistence
**What:** Labels persistes dans `~/.vmux/labels.json`.
```go
// Format: map[sessionID]string
type LabelStore struct {
mu sync.RWMutex
labels map[string]string
path string
}
func (ls *LabelStore) Set(sessionID, label string) error {
ls.mu.Lock()
defer ls.mu.Unlock()
ls.labels[sessionID] = label
return ls.save()
}
func (ls *LabelStore) save() error {
data, _ := json.MarshalIndent(ls.labels, "", " ")
return os.WriteFile(ls.path, data, 0o644)
}
```
### Pattern 7: Tracking des Transitions d'Etat
**What:** Le daemon enregistre le timestamp quand une session passe a NeedsInput.
```go
type SessionRegistry struct {
mu sync.RWMutex
sessions map[string]*TrackedSession // key = sessionID
}
type TrackedSession struct {
SessionInfo
PrevState string
WaitingSince time.Time // set quand state passe a NeedsInput
}
func (r *SessionRegistry) Update(info SessionInfo) {
r.mu.Lock()
defer r.mu.Unlock()
existing, ok := r.sessions[info.SessionID]
if !ok {
r.sessions[info.SessionID] = &TrackedSession{SessionInfo: info}
return
}
if info.State == "Needs Input" && existing.PrevState != "Needs Input" {
existing.WaitingSince = time.Now()
}
existing.PrevState = existing.State
existing.SessionInfo = info
if info.State == "Needs Input" {
info.WaitingSince = &existing.WaitingSince
}
}
```
### Anti-Patterns to Avoid
- **i3 GetTree() sans _NET_WM_PID :** Le Node i3 n'a PAS de champ PID. Il faut passer par X11 pour resoudre Window -> PID.
- **Relancer le daemon a chaque commande :** Utiliser autostart (connect, si echec start, re-connect).
- **Socket HTTP/REST :** Over-engineered. JSON Lines sur Unix socket suffit (une ligne = une requete, une ligne = une reponse).
- **Polling i3 GetTree a chaque scan :** Coupler le scan /proc avec le scan i3 dans la meme boucle de 5s pour avoir une vue coherente.
- **Stocker les labels par PID :** Les PID changent a chaque redemarrage de Claude Code. Utiliser le sessionID (UUID stable).
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| i3 IPC protocol | Parser binaire i3 IPC | `go.i3wm.org/i3/v4` | Protocole binaire complexe avec magic bytes, types, longueurs. Retry transparent integre. |
| X11 window properties | Shell-out vers xprop/xdotool | `BurntSushi/xgbutil/ewmh.WmPidGet()` | Deja en dep transitive. Pure Go, pas de fork/exec. |
| Process tree walking | Lib procfs | `os.ReadFile("/proc/PID/status")` | Une seule ligne a parser (PPid). Trivial. |
| JSON encoding/decoding | Lib serialisation | `encoding/json` stdlib | JSON Lines est trivial. |
| CLI sub-commands | Framework CLI (cobra) | `os.Args` manual dispatch | 5 sous-commandes max (`list`, `switch`, `label`, `stop`, `daemon`). Un switch/case suffit. |
**Insight :** La seule vraie complexite externe est i3 IPC (protocole binaire) et X11 (protocole bas niveau). Les deux sont couverts par les dependances de `go.i3wm.org/i3/v4`.
## Common Pitfalls
### Pitfall 1: i3 Node n'a pas de PID
**What goes wrong:** Le code suppose que `Node.PID` existe (comme dit dans D-04) et panic/compile error.
**Why it happens:** Confusion courante. i3 ne stocke pas le PID dans l'arbre. Le PID est une propriete X11 du client window.
**How to avoid:** Utiliser `Node.Window` (X11 window ID) + `ewmh.WmPidGet()` pour obtenir le terminal PID.
**Warning signs:** Champ inexistant dans la struct Node du Go package.
### Pitfall 2: Socket file residuel apres crash
**What goes wrong:** vmuxd ne demarre pas car `~/.vmux/vmux.sock` existe deja (bind: address already in use).
**Why it happens:** Crash ou kill -9 sans cleanup.
**How to avoid:** Avant `net.Listen("unix", path)`, tenter un `net.Dial("unix", path)`. Si echec -> supprimer le fichier stale. Si succes -> daemon deja actif.
**Warning signs:** "address already in use" au demarrage.
### Pitfall 3: Race condition daemon/CLI
**What goes wrong:** Le CLI envoie une requete avant que le daemon ait fini son premier scan.
**Why it happens:** Autostart : le daemon vient de demarrer, le registre est vide.
**How to avoid:** Le daemon fait un scan synchrone AVANT d'ecouter sur le socket. Le CLI attend que le socket soit pret (retry 50ms x 20).
**Warning signs:** `vmux list` retourne 0 sessions juste apres le premier lancement.
### Pitfall 4: i3 non disponible (SSH, TTY, Wayland)
**What goes wrong:** `i3.GetTree()` echoue car i3 IPC n'est pas accessible.
**Why it happens:** vmux est execute dans un environnement sans i3 (SSH, console, Sway/Wayland).
**How to avoid:** Attraper l'erreur de `GetTree()`, mettre workspace="" pour toutes les sessions. `vmux list` fonctionne toujours, sans colonne workspace. `vmux switch` retourne une erreur claire.
**Warning signs:** Erreur au premier scan.
### Pitfall 5: xgbutil necessita DISPLAY
**What goes wrong:** `xgbutil.NewConn()` echoue si `$DISPLAY` n'est pas set.
**Why it happens:** Meme situation que pitfall 4 : pas de X11.
**How to avoid:** Verifier `$DISPLAY` avant d'initialiser xgbutil. Si absent, desactiver le mapping workspace (fallback gracieux).
**Warning signs:** Erreur "cannot open display" au demarrage du daemon.
### Pitfall 6: Labels orphelins dans labels.json
**What goes wrong:** labels.json grossit indefiniment avec des sessionIDs de sessions terminees.
**Why it happens:** Les sessions Claude Code sont ephemeres, les sessionIDs changent.
**How to avoid:** Au chargement de labels.json, ne garder que les labels dont le sessionID correspond a une session active. Ou purger periodiquement (ex: garder les 100 derniers).
**Warning signs:** labels.json de plusieurs Mo apres des semaines d'utilisation.
### Pitfall 7: PPID chain walk infinie
**What goes wrong:** La boucle de walk PPID ne termine pas (process circulaire theorique).
**Why it happens:** Bug kernel theorique ou processus zombie.
**How to avoid:** Limiter a 20 iterations. PID 1 (init) est toujours le point d'arret.
**Warning signs:** CPU spike pendant le scan.
## Code Examples
### Initialisation X11 pour _NET_WM_PID
```go
// Source: BurntSushi/xgbutil/ewmh package (dep transitive de go.i3wm.org/i3/v4)
import (
"github.com/BurntSushi/xgb/xproto"
"github.com/BurntSushi/xgbutil"
"github.com/BurntSushi/xgbutil/ewmh"
"go.i3wm.org/i3/v4"
)
func buildTerminalWorkspaceMap() (map[int]string, error) {
xu, err := xgbutil.NewConn()
if err != nil {
return nil, fmt.Errorf("X11 connection failed: %w", err)
}
defer xu.Conn().Close()
tree, err := i3.GetTree()
if err != nil {
return nil, fmt.Errorf("i3 GetTree failed: %w", err)
}
result := make(map[int]string)
var walk func(node *i3.Node, wsName string)
walk = func(node *i3.Node, wsName string) {
if node.Type == i3.WorkspaceNode {
wsName = node.Name
}
if node.Window > 0 {
pid, err := ewmh.WmPidGet(xu, xproto.Window(node.Window))
if err == nil && pid > 0 {
result[int(pid)] = wsName
}
}
for _, child := range node.Nodes {
walk(child, wsName)
}
for _, child := range node.FloatingNodes {
walk(child, wsName)
}
}
walk(tree.Root, "")
return result, nil
}
```
### Lecture du PPID depuis /proc
```go
// Source: verification directe sur /proc//status
func readPPID(pid int) (int, error) {
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid))
if err != nil {
return 0, err
}
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "PPid:") {
fields := strings.Fields(line)
if len(fields) >= 2 {
return strconv.Atoi(fields[1])
}
}
}
return 0, fmt.Errorf("PPid not found for PID %d", pid)
}
```
### Unix Socket Server (daemon)
```go
// Source: Go stdlib net package
func startDaemonSocket(sockPath string) (net.Listener, error) {
// Cleanup stale socket
if _, err := net.DialTimeout("unix", sockPath, 200*time.Millisecond); err != nil {
os.Remove(sockPath) // stale, supprimer
}
os.MkdirAll(filepath.Dir(sockPath), 0o700)
return net.Listen("unix", sockPath)
}
func serveDaemon(listener net.Listener, registry *SessionRegistry) {
for {
conn, err := listener.Accept()
if err != nil {
return // listener ferme
}
go handleConnection(conn, registry)
}
}
func handleConnection(conn net.Conn, registry *SessionRegistry) {
defer conn.Close()
decoder := json.NewDecoder(conn)
encoder := json.NewEncoder(conn)
var req Request
if err := decoder.Decode(&req); err != nil {
return
}
switch req.Action {
case "list":
sessions := registry.List()
encoder.Encode(Response{OK: true, Sessions: sessions})
case "switch":
// ... fuzzy match + i3.RunCommand
case "label":
// ... set label
case "stop":
encoder.Encode(Response{OK: true})
// signal shutdown
}
}
```
### Switch workspace via i3
```go
// Source: go.i3wm.org/i3/v4 documentation (pkg.go.dev)
func switchToWorkspace(wsName string) error {
cmd := fmt.Sprintf("workspace number %s", wsName)
results, err := i3.RunCommand(cmd)
if err != nil {
if i3.IsUnsuccessful(err) {
return fmt.Errorf("i3 switch failed: %v", results[0].Error)
}
return err
}
return nil
}
```
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| BurntSushi/xgb (original) | jezek/xgb (maintained fork) | 2021 | jezek/xgb est le fork actif. Mais go.i3wm.org/i3/v4 utilise encore BurntSushi/xgb. |
| i3 IPC v4.0 (basique) | i3 IPC v4.24 (complet) | 2024 | Memes APIs core. GetTree, GetWorkspaces, RunCommand stables depuis des annees. |
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Go testing (stdlib) |
| Config file | Aucun (convention Go par defaut) |
| Quick run command | `go test ./...` |
| Full suite command | `go test -v -race ./...` |
### Phase Requirements -> Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| DISC-04 | Label humain sur une session | unit | `go test -run TestLabelStore -v` | Wave 0 |
| I3-01 | Mapping session -> workspace i3 | unit | `go test -run TestResolveWorkspace -v` | Wave 0 |
| I3-02 | Switch vers workspace i3 d'une session | unit+integration | `go test -run TestFuzzyMatch -v` | Wave 0 |
| STATE-04 | Temps ecoule depuis attente | unit | `go test -run TestWaitingSince -v` | Wave 0 |
### Sampling Rate
- **Per task commit:** `go test ./...`
- **Per wave merge:** `go test -v -race ./...`
- **Phase gate:** Full suite green before `/gsd:verify-work`
### Wave 0 Gaps
- [ ] `daemon_test.go` -- poll loop, registre, autostart logic (testable avec mock socket)
- [ ] `client_test.go` -- requetes/reponses sur socket (testable avec mock server)
- [ ] `protocol_test.go` -- serialisation JSON des requetes/reponses
- [ ] `workspace_test.go` -- couvre I3-01 : PPID chain walk (testable avec fake /proc + mock i3 tree)
- [ ] `i3bridge_test.go` -- couvre I3-02 : fuzzy match (pure logique, pas de mock)
### Strategy TDD
Le daemon est testable en isolant les couches :
- **Registre** : pure logique, pas de I/O. Tester les transitions d'etat, le tracking WaitingSince.
- **Protocol** : serialisation/deserialisation JSON. Tester les request/response structs.
- **Workspace resolution** : `readPPID` mockable via un fake /proc. Le mapping i3 mockable via une interface.
- **Client/Server** : Tester avec un vrai Unix socket dans un tmpdir.
- **i3 bridge** : Abstraire derriere une interface (`type I3Client interface { GetTree() ... }`). Mocker en test.
L'integration i3 reelle (GetTree, RunCommand) sera testee manuellement sur la machine. Les tests unitaires couvrent toute la logique sans dependre de i3.
## Environment Availability
| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| Go | Build & run | oui | 1.25.7 (via nix-shell) | - |
| i3 | Workspace mapping + switch | oui | 4.24 | Fallback gracieux : vmux fonctionne sans workspace si i3 absent |
| X11 (DISPLAY) | _NET_WM_PID resolution | oui | X11 actif (i3 tourne) | Meme fallback : pas de workspace sans X11 |
| /proc filesystem | Process detection + PPID chain | oui | procfs Linux 6.19 | - |
| xdotool | Non utilise (pure Go via xgbutil) | oui | 3.20211022.1 | - |
**Missing dependencies with no fallback:** Aucune.
**Missing dependencies with fallback:**
- i3 + X11 : si absents, vmux fonctionne sans les colonnes workspace et sans `vmux switch`. Toutes les autres fonctionnalites (list, label, temps d'attente) restent operationnelles.
## Open Questions
1. **Concurrence X11 + i3 IPC dans le daemon**
- Ce qu'on sait : `go.i3wm.org/i3/v4` est concurrent-safe. `xgbutil` n'est PAS concurrent-safe explicitement.
- Ce qui est flou : Le daemon poll dans une goroutine et repond aux requetes dans d'autres. La connexion X11 doit-elle etre re-ouverte a chaque scan ou partagee ?
- Recommandation : Ouvrir la connexion X11 une fois au demarrage du daemon. N'utiliser `ewmh.WmPidGet` que dans la goroutine de scan (jamais depuis les handlers de requetes). Le scan produit une snapshot `map[int]string` atomiquement remplacee via `sync.RWMutex`.
2. **Sous-commande `daemon` visible ou cachee ?**
- Ce qu'on sait : D-03 dit autostart implicite.
- Ce qui est flou : L'utilisateur peut-il lancer `vmux daemon` manuellement ?
- Recommandation : Sous-commande cachee (non affichee dans l'aide). Utile pour le debug (`vmux daemon --foreground`).
3. **Nettoyage des sessions disparues**
- Ce qu'on sait : Le daemon poll toutes les 5s. Si un processus Claude disparait, il ne sera plus dans /proc.
- Ce qui est flou : Faut-il garder l'entree quelques secondes pour eviter le flicker ?
- Recommandation : Supprimer immediatement du registre. Si le PID n'est plus dans /proc, la session est terminee.
## Sources
### Primary (HIGH confidence)
- [go.i3wm.org/i3/v4 pkg.go.dev](https://pkg.go.dev/go.i3wm.org/i3/v4) - API complete : Node struct (pas de PID), GetTree, GetWorkspaces, RunCommand, Subscribe
- [BurntSushi/xgbutil/ewmh source](https://github.com/BurntSushi/xgbutil/blob/master/ewmh/ewmh.go) - `WmPidGet(xu, win)` pour _NET_WM_PID
- Inspection directe machine : 6 Claude PIDs mappes a leurs workspaces via PPID chain
- i3-msg -t get_tree : confirme absence du champ `pid` dans les nodes (14 windows verifiees)
### Secondary (MEDIUM confidence)
- [i3 IPC documentation](https://i3wm.org/docs/ipc.html) - Protocole IPC officiel
- [Go Unix Domain Sockets (Eli Bendersky)](https://eli.thegreenplace.net/2019/unix-domain-sockets-in-go/) - Pattern daemon Go
- [go-i3 GitHub](https://github.com/i3/go-i3) - Repo officiel de la lib Go i3
### Tertiary (LOW confidence)
- Comportement de `xgbutil` en multi-thread : documentation limitee, recommandation basee sur les bonnes pratiques Go (single writer)
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH - go.i3wm.org/i3/v4 v4.24.0 verifie, BurntSushi/xgbutil confirme en dep transitive
- Architecture: HIGH - Algorithme PID->workspace verifie sur 6 sessions reelles
- Pitfalls: HIGH - Absence de PID dans Node i3 decouverte par inspection directe, pas par supposition
- Protocol: MEDIUM - Design du protocole socket base sur des patterns Go standard, pas de source specifique
**Research date:** 2026-03-23
**Valid until:** 2026-04-23 (i3 IPC stable depuis des annees, /proc stable)