A session in OpenCode is a persistent document whose canonical shape (Info schema) holds metadata like id, title, model, tokens, cost, and summary; serialization, forking, listing, and event emission are coordinated through packages/opencode/src/session/session.ts. The SystemPrompt service synthesizes context for the AI agent by injecting environment details (working directory, git status, date, platform), available project references, model-specific branding, and merged permission-filtered MCP server instructions into the system prompt.
packages/opencode/src/session/session.ts defines the Info Schema as the canonical session data shape, including fields for id, slug, projectID, workspaceID, directory, parentID, title, agent, model, version, cost, tokens, summary, share, metadata, revert, permission, and time.[1] GlobalInfo in packages/opencode/src/session/session.ts extends Info with a nullable project field (ProjectInfo | null), used for cross-project session listings.[1] ArchivedTimestamp in packages/opencode/src/session/session.ts is typed as Schema.Finite (not NonNegativeInt) to remain permissive toward negative values accepted by the legacy HTTP API while rejecting non-finite values that cannot round-trip through JSON.[1] When a session's tokens field is absent during toRow serialization, packages/opencode/src/session/session.ts substitutes EmptyTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } as the default token count object.[1]
CreateInput in packages/opencode/src/session/session.ts is an optional struct whose fields — parentID, title, agent, model, metadata, permission, and workspaceID — are each individually optional.[1] ListInput in packages/opencode/src/session/session.ts accepts directory, scope (only "project" allowed), path, workspaceID, roots, start, search, and limit for project-scoped session listing.[1] GlobalListInput in packages/opencode/src/session/session.ts extends the project-scoped listing shape with the additional cursor and archived fields for cross-project session listing.[1]
isDefaultTitle(title) in packages/opencode/src/session/session.ts returns true when a session title matches the auto-generated pattern "New session - <ISO timestamp>" or "Child session - <ISO timestamp>".[1] getForkedTitle in packages/opencode/src/session/session.ts increments an existing (fork #N) suffix or appends (fork #1) to titles that do not already carry one.[1]
packages/opencode/src/session/session.ts stores plan files under .opencode/plans/ relative to the worktree when the project has VCS, or under the global data path plans/ directory otherwise.[1]
packages/opencode/src/session/session.ts re-exports session lifecycle events (Created, Updated, Deleted, Diff, Error) from SessionV1.Event, keeping the v2 session layer backward-compatible with v1 event consumers.[1] session.create emits a SessionNs.Event.Created event on the EventV2Bridge service containing the new session's id, projectID, directory, path, and title fields.[2] The session.created event is guaranteed to be emitted before the session.updated event — indexOf("created") must be less than indexOf("updated") on the same event bus.[2] On session creation, a legacy global sync payload is also emitted on GlobalBus with payload.type === "sync" and a syncEvent whose type is the versioned SessionNs.Event.Created.type, seq is 0, and aggregateID equals the session ID.[2] EventV2Bridge calls GlobalBus.on("event", listener) to bridge V2 events onto the legacy global event bus.[2] session.updatePart with a step-finish part emits a MessageV2.Event.PartUpdated event carrying the full token breakdown (input, output, reasoning, total, cache.read, cache.write) and cost.[2] The PartUpdated event payload is a distinct copy of the input — receivedPart is not reference-equal to the original partInput object.[2]
session.remove succeeds even when no active instance exists for that session ID — its Effect.exit resolves as Exit.isSuccess.[2] After session.remove, subsequent session.get calls for that ID fail — the session is no longer retrievable.[2] session.fork copies the parent session's metadata by default; the forked metadata is deep-equal to the parent's but is a distinct object (not reference-equal).[2] When session.create is called without a metadata argument, info.metadata and the persisted saved.metadata are both undefined.[2] session.fork determines which messages to include in the fork prefix using real chronological order via time.created rather than ID lexicographic order — a correctness fix for v1.18.15.[2] session.fork in packages/opencode/src/session/session.ts branches from a parent session at a chosen message boundary, carrying a prefix of the parent's message history into the forked session.
The SystemPrompt service in packages/opencode/src/session/system.ts exposes three methods: environment (returns env/model context lines), skills (returns skill instructions for an agent), and mcp (returns MCP server instructions for an agent).[3] The environment method in packages/opencode/src/session/system.ts injects the current working directory, workspace root, git repo status, platform, today's date, and the model's provider/API IDs into the system prompt.[3] When any project references with descriptions exist, environment appends an <available_references> XML block listing each reference's name, path, and description; the block is omitted entirely when the list is empty.[3] For the Muse model family, packages/opencode/src/session/system.ts substitutes {{MODEL_NAME}} in PROMPT_META with either "Muse Glimmer" or "Muse Spark" depending on the model API ID.[3] The skills method in packages/opencode/src/session/system.ts returns undefined (omitting the skills block) when the "skill" permission is disabled for the agent.[3] When skills are included, skills renders them with verbose: true to improve agent ingestion of skill information — the system prompt receives more detail than the tool description.[3] The mcp method in packages/opencode/src/session/system.ts merges agent-level and session-level permission rulesets via Permission.merge, then excludes any MCP server whose tools are all disabled by the combined ruleset.[3] The SystemPrompt service layer depends on Skill.Service, MCP.Service, and LocationServiceMap.Service, declared via LayerNode.make with Skill.node, MCP.node, and a local locationServiceMapNode as its dependencies.[3]
The session test suite builds its Effect layer by composing SessionNs.node, EventV2Bridge.node, SessionProjector.node, CrossSpawnSpawner.node, and InstanceStore.node via AppNodeBuilder.build / LayerNode.group.[2] The step-finish token propagation test carries a 30-second timeout, indicating the event round-trip may take significant time under load.[2]
Canonical example of awaiting a session event with a 2-second timeout using Effect.race in session tests:
const awaitDeferred = <T>(deferred: Deferred.Deferred<T>, message: string) =>
Effect.race(
Deferred.await(deferred),
Effect.sleep("2 seconds").pipe(Effect.flatMap(() => Effect.fail(new Error(message)))),
)
Regression tests for tab navigation are maintained in packages/app/e2e/regression/subagent-child-navigation.spec.ts to validate the tab context menu rendering in packages/app/src/components/titlebar-tab-nav.tsx. Regression tests for session rename are maintained in packages/app/e2e/regression/session-rename.spec.ts to validate the session rename interaction in packages/app/src/pages/session/timeline/message-timeline.tsx.
Sources