Skip to content

Session Management

Eshan Roy edited this page Jun 16, 2026 · 2 revisions

Session Management

M31 Autonomous (M31A) persists all session state project-locally, enabling resume, checkpoint, and full conversation history across restarts.

Session Manager

Source: pkg/session/manager.go

Storage Layout

Sessions are stored project-locally in <workDir>/.m31a/:

<project>/.m31a/
├── session.json          # Session metadata (ID, model, phase, timestamps)
├── session.json.bak      # Backup of previous session
├── messages.json         # Full conversation history
├── checkpoint.json       # Latest workflow checkpoint
├── STATE.md              # Current workflow state markdown
├── TASKS.md              # Task list with statuses
├── backups/              # Auto-backups from file edits
│   ├── filename.ext.bak.1
│   └── filename.ext.bak.2
└── worktrees/            # Subagent worktree directories
    └── agent-<id>/

Global config remains in ~/.m31a/:

~/.m31a/
├── config.toml           # User configuration
├── LEDGER.md             # Cross-session learning ledger
├── recent_models.json    # Recent models + favorites
└── .force-exit           # Sentinel for unclean shutdown detection

Session Lifecycle

NewSession(model, provider)
    ├── Generate random hex ID (crypto/rand)
    ├── Create <workDir>/.m31a/ directory
    ├── Backup existing session.json → session.json.bak
    ├── Write session.json atomically
    ├── Write empty messages.json
    └── Add .m31a/ to .gitignore

LoadSession(id)
    ├── Read session.json (with 50MB size limit)
    ├── Validate ID, StartedAt, WorkflowPhase
    ├── Read messages.json
    └── Set ResumedAt timestamp

SaveSession(session)
    ├── Marshal session + messages
    └── Atomic write both files

Session Struct

Source: pkg/session/session.go

type Session struct {
    ID              string
    ParentID        string
    ChildrenIDs     []string
    Label           string
    Tags            []string
    Model           string
    Provider        string
    StartedAt       time.Time
    ResumedAt       *time.Time
    MessageCount    int
    WorkflowPhase   WorkflowPhase
    Project         *ProjectState
    Messages        []types.Message
    Goal            string
    Questions       []string
}

Session ID Generation

Session IDs are cryptographically random hex strings:

  • Default: 4 random bytes = 8 hex characters
  • Configurable via features.session_id_length
  • Generated using crypto/rand.Read()

Atomic Writes

All session writes use atomic write semantics:

Source: internal/fileutil/atomic.go

  1. Write data to a temp file in the same directory
  2. fsync() the temp file
  3. Rename temp file to target path
  4. This prevents corruption on crash or power loss

Size Limits

Session files are capped at MaxSessionFileSize = 50 MB to prevent OOM from corrupted or malicious data:

func readFileLimited(path string, maxBytes int64) ([]byte, error) {
    // Stat check + LimitReader double-protection
}

Checkpoints

Source: pkg/session/checkpoint.go

type Checkpoint struct {
    Phase     WorkflowPhase
    Timestamp time.Time
    Metadata  map[string]string
}

Checkpoints are saved:

  • On every phase transition
  • Manually via /save or /session checkpoint
  • Automatically during workflow execution

Workflow State Persistence

Source: pkg/session/planning.go

The session manager persists workflow state to session.json:

State Description
Goal The user's original goal text
Phase Current workflow phase
Questions Discuss phase questions

Task Persistence

Tasks from the Plan phase are saved to TASKS.md and can be loaded back:

func (m *Manager) SaveTasks(sessionID string, tasks []types.Task) error
func (m *Manager) LoadTasks(sessionID string) ([]types.Task, error)

Session Info

Source: pkg/session/session_info.go

type SessionInfo struct {
    ID            string
    Model         string
    Provider      string
    StartedAt     time.Time
    LastModified  time.Time
    MessageCount  int
    WorkflowPhase WorkflowPhase
    Label         string
    Corrupted     bool
}

Used for session listing and display without loading full message history.

Resume on Startup

When features.resume_on_startup = true, M31A automatically resumes the most recent session:

// In main.go:
if cfg.Features.ResumeOnStartup {
    sessions, err := sessionMgr.ListSessions()
    if err == nil && len(sessions) > 0 {
        app.SetResumeSessionID(sessions[0].ID)
    }
}

The TUI's Init() method calls loadAndRestoreSession() to restore messages, workflow state, and model selection.

Recent Models

Source: Global config in ~/.m31a/recent_models.json

type RecentModelsData struct {
    Recent    []string        `json:"recent"`
    Favorites map[string]bool `json:"favorites"`
}
  • Tracks up to maxRecentModels (default 10) recently used models
  • Supports favoriting models for quick access
  • Stored globally (not per-project)

Session Export

Sessions can be exported in two formats:

Format Command Output
Markdown /export markdown [path] Human-readable conversation
JSON /export json [path] Machine-readable full data

Git Integration

The session manager automatically adds .m31a/ to .gitignore when creating sessions:

func (m *Manager) ensureGitIgnore() {
    // Appends ".m31a/" to .gitignore if not present
}

Session Retention

Configurable via features.session_retention_days (default 30). The Cleanup() method is called during Init() but is currently a no-op for project-local sessions (to avoid auto-deleting project data).

Clone this wiki locally