Skip to content

Session Management

Eshan Roy edited this page Jun 23, 2026 · 3 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).

Session Coordinator

Source: pkg/coordinator/coordinator.go

The session coordinator manages concurrent execution of sessions, preventing corruption from parallel access to the same session.

Demand Coalescing

When multiple drains are requested for the same session key, the coordinator coalesces them:

  • At most one drain runs per key at a time
  • Additional Run() calls return a context that fires when the current drain completes
  • Wake() signals that new work may be available without blocking
  • Interrupt() cancels the current drain

File Locking

The coordinator works with platform-specific file locking (Unix: flock, Windows: direct write) to prevent concurrent writes to session files.

Session Compaction

Source: pkg/compaction/compaction.go

When conversations grow large, the session compactor automatically summarizes old messages to prevent context window overflow.

How It Works

  1. Token estimation — Uses tiktoken-based estimation with EMA self-calibration
  2. Threshold check — Triggers when estimatedTokens > contextLength - buffer
  3. Message splitting — Divides messages into head (to summarize) and recent (to keep verbatim)
  4. LLM summarization — Generates a structured summary via the configured model
  5. Replacement — Replaces old messages with a single compaction summary message

Configuration

[compaction]
auto = true          # Enable automatic compaction
buffer = 20000       # Tokens reserved before compaction triggers
keep_tokens = 8000   # Tokens of recent history to preserve verbatim

Protected Content

The following are preserved in the recent window and never compressed:

  • Initial goal message
  • All system messages
  • Messages with tool calls
  • Tool result messages
  • Last 8,000 tokens of conversation

Clone this wiki locally