-
-
Notifications
You must be signed in to change notification settings - Fork 0
Session Management
M31A persists all session state project-locally, enabling resume, checkpoint, and full conversation history across restarts.
Source: pkg/session/manager.go
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
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
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 IDs are cryptographically random hex strings:
- Default: 4 random bytes = 8 hex characters
- Configurable via
features.session_id_length - Generated using
crypto/rand.Read()
All session writes use atomic write semantics:
Source: internal/fileutil/atomic.go
- Write data to a temp file in the same directory
-
fsync()the temp file - Rename temp file to target path
- This prevents corruption on crash or power loss
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
}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
/saveor/session checkpoint - Automatically during workflow execution
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 |
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)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.
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.
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)
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 |
The session manager automatically adds .m31a/ to .gitignore when creating sessions:
func (m *Manager) ensureGitIgnore() {
// Appends ".m31a/" to .gitignore if not present
}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).