Files
antidrift/internal/session/session.go
T
2026-05-31 11:45:46 -04:00

157 lines
4.2 KiB
Go

// Package session owns the daemon's in-memory state of truth and persists a
// snapshot on every change. Transitions go through the pure statemachine.
package session
import (
"sync"
"time"
"antidrift/internal/domain"
"antidrift/internal/statemachine"
"antidrift/internal/store"
)
// Controller holds runtime state and the active commitment behind a mutex.
type Controller struct {
mu sync.Mutex
runtimeState domain.RuntimeState
commitment *domain.Commitment
deadline time.Time
snapshotPath string
}
// CommitmentView is the UI projection of the active commitment.
type CommitmentView struct {
NextAction string `json:"next_action"`
SuccessCondition string `json:"success_condition"`
TimeboxSecs int64 `json:"timebox_secs"`
DeadlineUnixSecs int64 `json:"deadline_unix_secs"`
}
// State is the broadcastable view of the controller.
type State struct {
RuntimeState domain.RuntimeState `json:"runtime_state"`
Commitment *CommitmentView `json:"commitment"`
}
// New loads any persisted snapshot, or starts Locked.
func New(snapshotPath string) (*Controller, error) {
s, err := store.Load(snapshotPath)
if err != nil {
return nil, err
}
c := &Controller{
runtimeState: s.RuntimeState,
commitment: s.Commitment,
snapshotPath: snapshotPath,
}
if s.DeadlineUnixSecs > 0 {
c.deadline = time.Unix(s.DeadlineUnixSecs, 0)
}
if c.runtimeState == "" {
c.runtimeState = domain.RuntimeLocked
}
return c, nil
}
// State returns the current broadcastable state. Safe for concurrent use.
func (c *Controller) State() State {
c.mu.Lock()
defer c.mu.Unlock()
return c.stateLocked()
}
// Deadline returns the active commitment deadline, or the zero time.
func (c *Controller) Deadline() time.Time {
c.mu.Lock()
defer c.mu.Unlock()
return c.deadline
}
func (c *Controller) stateLocked() State {
st := State{RuntimeState: c.runtimeState}
if c.commitment != nil {
st.Commitment = &CommitmentView{
NextAction: c.commitment.NextAction,
SuccessCondition: c.commitment.SuccessCondition,
TimeboxSecs: c.commitment.TimeboxSecs,
DeadlineUnixSecs: c.deadline.Unix(),
}
}
return st
}
func (c *Controller) persistLocked() error {
snap := store.Snapshot{RuntimeState: c.runtimeState, Commitment: c.commitment}
if !c.deadline.IsZero() {
snap.DeadlineUnixSecs = c.deadline.Unix()
}
return store.Save(c.snapshotPath, snap)
}
// EnterPlanning moves Locked -> Planning.
func (c *Controller) EnterPlanning() error {
c.mu.Lock()
defer c.mu.Unlock()
next, err := statemachine.TransitionRuntime(c.runtimeState, statemachine.EnterPlanning)
if err != nil {
return err
}
c.runtimeState = next
return c.persistLocked()
}
// StartManualCommitment validates input, activates a new commitment, and moves
// Planning -> Active.
func (c *Controller) StartManualCommitment(nextAction, successCondition string, timebox time.Duration) error {
c.mu.Lock()
defer c.mu.Unlock()
commitment, err := domain.NewManual(nextAction, successCondition, timebox)
if err != nil {
return err
}
commitment.State, err = statemachine.TransitionCommitment(commitment.State, statemachine.CommitmentActivate)
if err != nil {
return err
}
next, err := statemachine.TransitionRuntime(c.runtimeState, statemachine.ActivateAccepted)
if err != nil {
return err
}
c.runtimeState = next
c.commitment = &commitment
c.deadline = time.Now().Add(timebox)
return c.persistLocked()
}
// Complete moves Active -> Review and marks the commitment completed.
func (c *Controller) Complete() error {
c.mu.Lock()
defer c.mu.Unlock()
next, err := statemachine.TransitionRuntime(c.runtimeState, statemachine.CompleteForReview)
if err != nil {
return err
}
c.runtimeState = next
if c.commitment != nil {
if s, e := statemachine.TransitionCommitment(c.commitment.State, statemachine.Complete); e == nil {
c.commitment.State = s
}
}
return c.persistLocked()
}
// End moves Review -> Locked and clears the commitment.
func (c *Controller) End() error {
c.mu.Lock()
defer c.mu.Unlock()
next, err := statemachine.TransitionRuntime(c.runtimeState, statemachine.EndWorkPeriod)
if err != nil {
return err
}
c.runtimeState = next
c.commitment = nil
c.deadline = time.Time{}
return c.persistLocked()
}