Sessions track the history of interactions between the user and the AI, enabling context continuity across multiple turns.
The Store type (internal/session/store.go) manages session persistence:
type Store struct {
workingDir string
rootDir string
namespaceDir string
}
func NewStore(workingDir string) (*Store, error)Sessions are stored under ~/.keen/sessions/ with namespace subdirectories.
~/.keen/sessions/
├── project-path-hash/ # Namespace by working directory
│ ├── session-id-1/ # Each session has its own directory
│ │ └── transcript_events.jsonl
│ ├── session-id-2/
│ │ └── transcript_events.jsonl
│ └── ...
// internal/session/path.go
func sessionsRootDir() (string, error)
func namespaceDirName(workingDir string) string // e.g., "my-project-a1b2c3d4e5"
func sessionDirName(sessionID string) stringNamespace names are derived from the working directory:
- Sanitized:
/→-,\→- - Hashed: SHA1 prefix (10 chars) for uniqueness
type Session struct {
ID string
CreatedAt time.Time
Directory string
TranscriptPath string
nextSeq uint64
}
type LoadedSession struct {
Summary Summary
Events []Event
Session *Session
}Sessions are event-sourced. Each interaction is recorded as an event in a JSONL transcript file:
type Event struct {
Seq uint64 `json:"seq"`
Kind EventKind `json:"kind"`
// Payload based on Kind...
}| Kind | Description |
|---|---|
session_started |
Session created with metadata |
user_message |
User input |
assistant_turn |
AI response with transcript |
compaction_applied |
Context compaction applied |
type SessionStartedPayload struct {
SessionID string `json:"session_id"`
CreatedAt time.Time `json:"created_at"`
CWD string `json:"cwd"`
}
type MessagePayload struct {
Content string `json:"content"`
}
type AssistantTurnPayload struct {
Transcript []TranscriptItem `json:"transcript,omitempty"`
Message string `json:"message,omitempty"`
TurnMemory *llm.TurnMemory `json:"turn_memory,omitempty"`
Interrupted bool `json:"interrupted,omitempty"`
Error string `json:"error,omitempty"`
}Assistant turns include a transcript of what happened:
type TranscriptItem struct {
Kind TranscriptItemKind `json:"kind"`
Content string `json:"content,omitempty"`
ToolStart *ToolStartPayload `json:"tool_start,omitempty"`
ToolEnd *ToolEndPayload `json:"tool_end,omitempty"`
Bash *BashPayload `json:"bash,omitempty"`
Diff *DiffPayload `json:"diff,omitempty"`
}
const (
TranscriptItemText TranscriptItemKind = "text"
TranscriptItemReasoning TranscriptItemKind = "reasoning"
TranscriptItemToolStart TranscriptItemKind = "tool_start"
TranscriptItemToolEnd TranscriptItemKind = "tool_end"
TranscriptItemBash TranscriptItemKind = "bash"
TranscriptItemDiff TranscriptItemKind = "diff"
)func (s *Store) Create() (*Session, error)Creates a new session directory and writes a session_started event.
func (s *Store) Append(session *Session, event Event) errorAppends an event to the transcript JSONL file. Each event gets an incrementing sequence number.
func (s *Store) List() ([]Summary, error)Returns summaries of all sessions in the namespace, sorted by most recently updated.
func (s *Store) Load(summary Summary) (*LoadedSession, error)Loads all events from a session's transcript file.
Summaries provide lightweight session metadata for listing:
type Summary struct {
ID string
CreatedAt time.Time
UpdatedAt time.Time
LastUserMessage string
Directory string
TranscriptPath string
LastSeq uint64
}The projection.go file provides utilities to rebuild conversation history from events:
func BuildConversation(events []Event) []llm.MessageThis reconstructs the message list needed for LLM context, handling:
user_messageevents → user role messagesassistant_turnevents → assistant role messages (with turn memory)compaction_appliedevents → replaces messages with compacted version
Session IDs are version 4 UUIDs:
func generateSessionID() (string, error)Generated using crypto/rand with appropriate version/variant bits set.
The transcript is a JSONL file (JSON Lines) where each line is a single JSON event:
{"seq":1,"kind":"session_started",...}
{"seq":2,"kind":"user_message",...}
{"seq":3,"kind":"assistant_turn",...}
...
This format allows efficient appending and partial loading.