feat(lifecycle): validate shared symlinks at daemon startup (A2)
Wire symlinks.ValidateAll into the lifecycle manager so the daemon refuses to start if any configured account is missing one of the shared-state symlinks or if a link diverges from the canonical target. Previously, a missing link on a freshly deployed VM would silently create a divergent state tree per account (duplicate JSONL transcripts, broken undo history) — exactly the failure mode the symlinks package (A1) was introduced to prevent. The check runs once at startup before EnsureAllSessions, guarding a single well-defined invariant: "every account home shares the same projects/, file-history/ and session-env/ roots". No auto-heal on divergence — we fail fast with an explicit error so the operator fixes it manually rather than one account's state being overwritten. Part of Phase 1 Chantier A — Failover robuste. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
91091d7abf
commit
e16e3526a0
3 changed files with 68 additions and 1 deletions
|
|
@ -4,11 +4,13 @@ package lifecycle
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"forge.secuaas.ovh/olivier/claude-failover/internal/config"
|
||||
"forge.secuaas.ovh/olivier/claude-failover/internal/state"
|
||||
"forge.secuaas.ovh/olivier/claude-failover/internal/symlinks"
|
||||
"forge.secuaas.ovh/olivier/claude-failover/internal/tmux"
|
||||
)
|
||||
|
||||
|
|
@ -47,6 +49,35 @@ func (m *Manager) Run(ctx context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// ValidateSharedSymlinks verifies that every configured account home has
|
||||
// the three shared-state symlinks (session-env, file-history, projects)
|
||||
// in place and pointing at the canonical shared targets.
|
||||
//
|
||||
// Called once at daemon startup BEFORE sessions are recreated. A missing
|
||||
// or divergent link would silently fork the state tree between the two
|
||||
// accounts, breaking failover. We fail fast so the operator fixes it
|
||||
// before any work is in flight.
|
||||
//
|
||||
// EnsureForAccount creates missing links but refuses to touch divergent
|
||||
// ones — see internal/symlinks for the rationale.
|
||||
func (m *Manager) ValidateSharedSymlinks() error {
|
||||
if len(m.config.Accounts) == 0 {
|
||||
return fmt.Errorf("[lifecycle] no accounts configured — cannot validate shared symlinks")
|
||||
}
|
||||
homes := make([]string, 0, len(m.config.Accounts))
|
||||
for _, acc := range m.config.Accounts {
|
||||
if acc.Home == "" {
|
||||
return fmt.Errorf("[lifecycle] account %q has empty home — refusing to continue", acc.Name)
|
||||
}
|
||||
homes = append(homes, acc.Home)
|
||||
}
|
||||
if err := symlinks.ValidateAll(homes, symlinks.RequiredShared); err != nil {
|
||||
return fmt.Errorf("shared symlinks invalid, refusing to start: %w", err)
|
||||
}
|
||||
m.logger.Printf("[lifecycle] shared symlinks OK for %d account(s)", len(homes))
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureAllSessions creates all configured sessions that are not yet present in tmux.
|
||||
// It is intended to be called once at daemon startup before Run is launched.
|
||||
func (m *Manager) EnsureAllSessions() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue