Session entries form a tree through id/parentId links. Their logical JSON shape
is separate from the file's physical storage format.
Base Context uses native-framed journals and derived indexes. The .jsonl
extension is retained, but a native journal is not a plain JSONL transcript. Do not
split it into text lines, use jq on it, or append/edit records by hand. Use the
native session owner and bounded asynchronous history reads described in the
SDK guide and the SessionManager API below.
The default product paths are:
~/.base-context/sessions/<session-id>.jsonl
~/.base-context/session-artifacts/<session-id>/
BASE_CONTEXT_HOME selects the product root. BASE_CONTEXT_SESSION_DIR can select
an absolute sessions directory. Use SessionManager.getSessionArtifactDir() for
the owned artifact path; do not infer that it always uses the default location.
See Session Storage.
Use the supported session deletion controls in
Resuming and Deleting Sessions, rather
than removing only a journal while its owner or related state remains active.
The old Prime Agent path ~/.prime/agent/sessions/ is not Base Context's product root.
Ordinary flat JSONL is a legacy import format, not the native storage format. Use explicit offline import. Importing retains source data; it does not turn old execution claims into live native authority. Do not place a legacy JSONL file in the native directory and assume opening it will perform an implicit migration.
The logical session header versions are distinct from native framing, ownership and RPC versions:
- Version 1: legacy linear entry sequence.
- Version 2: tree structure with
id/parentIdlinks. - Version 3:
hookMessagerenamed tocustom.
The explicit import path documents which older headers it accepts and converts. A version-3 header alone does not establish whether the containing file is plain JSONL or native-framed. The entry examples below describe logical payloads, not bytes that can be appended directly to a native journal.
session-manager.ts- Session entry types andSessionManagermessages.ts- Extended message types (BashExecutionMessage,CustomMessage, and others)packages/ai/src/types.ts- Base message types (UserMessage,AssistantMessage,ToolResultMessage)packages/agent/src/types.ts-AgentMessageunion type
For TypeScript definitions in an installed Base Context project, inspect node_modules/@ponythewhite/base-context/dist/ and node_modules/@ponythewhite/base-context-ai/dist/.
Session entries contain AgentMessage objects. Understanding these types is essential for parsing sessions and writing extensions.
Messages contain arrays of typed content blocks:
interface TextContent {
type: "text";
text: string;
}
interface ImageContent {
type: "image";
data: string; // base64 encoded
mimeType: string; // e.g., "image/jpeg", "image/png"
}
interface ThinkingContent {
type: "thinking";
thinking: string;
}
interface ToolCall {
type: "toolCall";
id: string;
name: string;
arguments: Record<string, any>;
}interface UserMessage {
role: "user";
content: string | (TextContent | ImageContent)[];
timestamp: number; // Unix ms
}
interface AssistantMessage {
role: "assistant";
content: (TextContent | ThinkingContent | ToolCall)[];
api: string;
provider: string;
model: string;
usage: Usage;
stopReason: "stop" | "length" | "toolUse" | "error" | "aborted";
errorMessage?: string;
timestamp: number;
}
interface ToolResultMessage {
role: "toolResult";
toolCallId: string;
toolName: string;
content: (TextContent | ImageContent)[];
details?: any; // Tool-specific metadata
isError: boolean;
timestamp: number;
}
interface Usage {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
totalTokens: number;
cost: {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
total: number;
};
}interface BashExecutionMessage {
role: "bashExecution";
command: string;
output: string;
exitCode: number | undefined;
cancelled: boolean;
truncated: boolean;
fullOutputPath?: string;
excludeFromContext?: boolean; // true for !! prefix commands
timestamp: number;
}
interface CustomMessage {
role: "custom";
customType: string; // Extension identifier
content: string | (TextContent | ImageContent)[];
display: boolean; // Show in TUI
details?: any; // Extension-specific metadata
timestamp: number;
}
interface BranchSummaryMessage {
role: "branchSummary";
summary: string;
fromId: string; // Entry we branched from
timestamp: number;
}
interface CompactionSummaryMessage {
role: "compactionSummary";
summary: string;
tokensBefore: number;
timestamp: number;
}type AgentMessage =
| UserMessage
| AssistantMessage
| ToolResultMessage
| BashExecutionMessage
| CustomMessage
| BranchSummaryMessage
| CompactionSummaryMessage;All entries (except SessionHeader) extend SessionEntryBase:
interface SessionEntryBase {
type: string;
id: string; // 8-char hex ID
parentId: string | null; // Parent entry ID (null for first entry)
timestamp: string; // ISO timestamp
}First line of the file. Metadata only, not part of the tree (no id/parentId).
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}For sessions with a parent (created via /fork, /clone, or newSession({ parentSession })):
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","parentSession":"/path/to/original/session.jsonl"}A message in the conversation. The message field contains an AgentMessage.
{"type":"message","id":"a1b2c3d4","parentId":"prev1234","timestamp":"2024-12-03T14:00:01.000Z","message":{"role":"user","content":"Hello"}}
{"type":"message","id":"b2c3d4e5","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:00:02.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Hi!"}],"provider":"anthropic","model":"claude-sonnet-4-5","usage":{...},"stopReason":"stop"}}
{"type":"message","id":"c3d4e5f6","parentId":"b2c3d4e5","timestamp":"2024-12-03T14:00:03.000Z","message":{"role":"toolResult","toolCallId":"call_123","toolName":"bash","content":[{"type":"text","text":"output"}],"isError":false}}Emitted when the user switches models mid-session.
{"type":"model_change","id":"d4e5f6g7","parentId":"c3d4e5f6","timestamp":"2024-12-03T14:05:00.000Z","provider":"openai","modelId":"gpt-4o"}Emitted when the user changes the thinking/reasoning level.
{"type":"thinking_level_change","id":"e5f6g7h8","parentId":"d4e5f6g7","timestamp":"2024-12-03T14:06:00.000Z","thinkingLevel":"high"}Emitted when the user changes provider service tier.
{"type":"service_tier_change","id":"e6f7g8h9","parentId":"e5f6g7h8","timestamp":"2024-12-03T14:07:00.000Z","serviceTier":"priority"}Created when context is compacted. Stores a summary of earlier messages.
{"type":"compaction","id":"f6g7h8i9","parentId":"e5f6g7h8","timestamp":"2024-12-03T14:10:00.000Z","summary":"User discussed X, Y, Z...","firstKeptEntryId":"c3d4e5f6","tokensBefore":50000}Optional fields:
details: Implementation-specific data (e.g.,{ readFiles: string[], modifiedFiles: string[] }for default, or custom data for extensions)fromHook:trueif generated by an extension,false/undefinedif generated by Base Context (legacy field name)
Created when switching branches via /tree with an LLM generated summary of the left branch up to the common ancestor. Captures context from the abandoned path.
{"type":"branch_summary","id":"g7h8i9j0","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:15:00.000Z","fromId":"f6g7h8i9","summary":"Branch explored approach A..."}Optional fields:
details: File tracking data ({ readFiles: string[], modifiedFiles: string[] }) for default, or custom data for extensionsfromHook:trueif generated by an extension,false/undefinedif generated by Base Context (legacy field name)
Extension state persistence. Does NOT participate in LLM context.
{"type":"custom","id":"h8i9j0k1","parentId":"g7h8i9j0","timestamp":"2024-12-03T14:20:00.000Z","customType":"my-extension","data":{"count":42}}Use customType to identify your extension's entries on reload.
Records RLM child usage folded into a parent assistant message. This entry is daemon bookkeeping and does not enter model context.
interface ChildUsageAttributionEntry extends SessionEntryBase {
type: "child_usage_attributed";
targetId: string; // Parent assistant message entry
childUsage: Usage; // Usage added by one child
aggregateUsage: Usage; // Updated parent aggregate
}Reload applies aggregateUsage to the target assistant message. Context-tree accounting can then subtract childUsage when reporting the parent node's own usage.
Extension-injected messages that DO participate in LLM context.
{"type":"custom_message","id":"i9j0k1l2","parentId":"h8i9j0k1","timestamp":"2024-12-03T14:25:00.000Z","customType":"my-extension","content":"Injected context...","display":true}Fields:
content: String or(TextContent | ImageContent)[](same as UserMessage)display:true= show in TUI with distinct styling,false= hiddendetails: Optional extension-specific metadata (not sent to LLM)
User-defined bookmark/marker on an entry.
{"type":"label","id":"j0k1l2m3","parentId":"i9j0k1l2","timestamp":"2024-12-03T14:30:00.000Z","targetId":"a1b2c3d4","label":"checkpoint-1"}Set label to undefined to clear a label.
Session metadata (e.g., user-defined display name). Set via /name command or pi.setSessionName() in extensions.
{"type":"session_info","id":"k1l2m3n4","parentId":"j0k1l2m3","timestamp":"2024-12-03T14:35:00.000Z","name":"Refactor auth module"}The session name is displayed in the session selector (/resume) instead of the first message when set.
Stores daemon-managed lifecycle state. Current persisted states are active, archived, and legacy crash; older sleep values normalize to archived when read.
Stores the latest short agent status shown in the agents view, including its summary, optional task state, and source message count. It does not enter model context.
Stores an append-only repository-state snapshot for agent status and recovery views. It does not enter model context.
Entries form a tree:
- First entry has
parentId: null - Each subsequent entry points to its parent via
parentId - Branching creates new children from an earlier entry
- The "leaf" is the current position in the tree
[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf
│
└─ [branch_summary] ─── [user msg] ← alternate branch
For explicit resident views, buildSessionContext() walks from the current leaf
to the root. The following describes logical message reconstruction, not a native
journal parser or the full native working-set selection contract. On indexed owned
sessions, use await AgentSession.buildSessionContext() for the native working
context, or the bounded asynchronous SessionManager reads below for detached history:
- Collects all entries on the path
- Extracts current model and thinking level settings
- If a
CompactionEntryis on the path:- Emits the summary first
- Then messages from
firstKeptEntryIdto compaction - Then messages after compaction
- Converts
BranchSummaryEntryandCustomMessageEntryto appropriate message formats
Bookkeeping entries such as child usage attribution, session lifecycle, agent status, and git state are ignored when building model context.
This whole-file example is only for a small, coherent offline legacy JSONL file. It is not a native journal reader or an import implementation. Use the explicit import command for migration and native bounded reads for owned session history.
import { readFileSync } from "fs";
const lines = readFileSync("legacy-session.jsonl", "utf8").trim().split("\n");
for (const line of lines) {
const entry = JSON.parse(line);
switch (entry.type) {
case "session":
console.log(`Session v${entry.version ?? 1}: ${entry.id}`);
break;
case "message":
console.log(`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`);
break;
case "compaction":
console.log(`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`);
break;
case "branch_summary":
console.log(`[${entry.id}] Branch from ${entry.fromId}`);
break;
case "custom":
console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);
break;
case "custom_message":
console.log(`[${entry.id}] Extension message (${entry.customType}): ${entry.content}`);
break;
case "label":
console.log(`[${entry.id}] Label "${entry.label}" on ${entry.targetId}`);
break;
case "model_change":
console.log(`[${entry.id}] Model: ${entry.provider}/${entry.modelId}`);
break;
case "thinking_level_change":
console.log(`[${entry.id}] Thinking: ${entry.thinkingLevel}`);
break;
}
}Key methods for working with sessions programmatically. Await asynchronous creation, listing, historical reads and writes as shown in the SDK. The synchronous metadata and explicit resident-view exceptions are noted below.
SessionManager.create(cwd, sessionDir?)- New sessionSessionManager.open(path, sessionDir?)- Open existing session fileSessionManager.continueRecent(cwd, sessionDir?)- Continue most recent or create newSessionManager.inMemory(cwd?)- No file persistenceSessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)- Fork session from another project
SessionManager.list(cwd, sessionDir?, callbacks?)- List sessions for a directorySessionManager.listAll(callbacks?, sessionDir?)- List all sessions across all projects
callbacks can provide onProgress(loaded, total) and onSession(session) handlers.
newSession(options?)- Start a new session (options:{ parentSession?: string })setSessionFile(path)- Switch to a different session filecreateBranchedSession(leafId)- Extract branch to new session file
appendMessage(message)- Add messageappendThinkingLevelChange(level)- Record thinking changeappendServiceTierChange(tier)- Record provider service-tier changeappendModelChange(provider, modelId)- Record model changeappendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?, customInstructions?)- Add compactionappendCustomEntry(customType, data?)- Extension state (not in context)appendChildUsageAttribution(targetId, childUsage, aggregateUsage)- Persist RLM child usage folded into a parent assistant messageappendSessionInfo(name)- Set session display nameappendSessionState(state)- Record daemon-managed lifecycle stateappendAgentStatus(status)- Record an agents-view status summaryappendGitState(git)- Record repository stateappendCustomMessageEntry(customType, content, display, details?)- Extension message (in context)appendLabelChange(targetId, label)- Set/clear label
Use await for historical reads and mutations on owned sessions. Complete reads default to 16,384 source entries and 64 MiB of source data. They return the complete requested result or raise a limit error. Each read captures its source; readBranches() shares one capture across multiple paths.
getLeafId()- Current position (synchronous metadata)readLeafEntry(maxSourceBytes?)- Current leaf entryreadEntry(id, maxSourceBytes?)- Detached entry by IDreadBranch(fromId?, limits?)- Complete root-to-leaf parent pathreadBranches(leafIds, limits?)- Multiple paths with shared source-entry accountingreadTree(limits?)- Complete tree structurereadLabel(id, maxSourceBytes?)- Label for an entrybranchTo(entryId)- Move the leaf; pass null for the position before any entriesbranchWithSummary(entryId, summary, details?, fromHook?)- Branch with context summary
Synchronous body getters such as getEntry(), getBranch(), getEntries(), and buildSessionContext() are resident-view APIs only. They reject indexed owned sessions. Explicit inMemory() and openReadOnly() views retain their resident behavior. Use AgentSession.buildSessionContext() for the native asynchronous working-context rebuild.
readEntries(limits?)- Complete detached entries (excluding header)readEntryRetention(id)- Source qualification; absence does not establish authorshipsupportsCapturedHistoryReads()- Whether this Manager supports owned captured readsgetHeader()- Session header metadatagetSessionName()- Get display name from latest session_info entrygetCwd()- Working directorygetSessionDir()- Session storage directorygetSessionId()- Session UUIDgetSessionFile()- Session file path (undefined for in-memory)isPersisted()- Whether session is saved to disk