Skip to content

SessionFs provider and state-file lifecycle

The SessionFs abstraction sits underneath session persistence and workspace artifacts in the extracted Copilot CLI bundle. It covers local session-state storage, SDK/RPC-backed filesystem providers, reverse calls, workspace files, large-output spill files, and fork-time state copying.

It fills a gap left by Session manager and event replay, which explains event-sourced sessions at a high level, and API and session event schema contracts, which lists the JSON-RPC method surface.

Source anchors

app.js is bundled/minified, so symbols are lookup aids for this extracted build rather than stable public API.

AreaSemantic aliasMinified anchorApprox. lineWhat it does
JSONL session storeSessionEventLogStoreIp=HHr(..., "session", ...)236, 4396Reads, appends, truncates, and lists events.jsonl through a SessionFs instance.
Base filesystem abstractionSessionFsBaseZge555Defines path joining, path conventions, and lock keys.
Local backendLocalSessionFsVC extends Zge555Delegates local file operations to native Do.sessionFsLocal* helpers.
RPC backendRpcSessionFsi1t extends Zge6096Delegates file operations through native Do.sessionFsRpc* helpers and reverse calls to the SDK client.
Workspace artifactsWorkspaceManagerqq3559-3573Manages workspace.yaml, plan.md, checkpoints/, files/, research/, and session.db.
Runtime session constructorsession sessionFs selectionthis.sessionFs = r.sessionFs ?? ...4471Uses an injected SessionFs, an event-log directory override, or the local default.
Workspace enablementinfinite-session workspace setupthis.sessionFs.sessionStatePath && new qq(...)4475Enables workspace artifacts only when the filesystem exposes a sessionStatePath.
Tool integrationlarge-output and shell buffersLW(...), BW(...), rAe(...)555, 567, 4481, 5654, 5691Writes large tool/shell output through the active session filesystem.
Session manager factoryper-session filesystem factorysessionFsFactory, setSessionFsFactory(...)5756Creates a filesystem for each session and lets server-mode clients replace the factory before sessions exist.
Fork state copysession-state cloningforkSession(...), copyForkedSessionState(...)5756Copies selected workspace artifacts and rewrites checkpoint paths when forking.
Server RPC provider registrationsessionFs.setProvidercreateSessionFsApi()6096-6100Lets one SDK/RPC connection become the process-wide session filesystem provider.
SDK generated handlersregisterClientSessionApiHandlers(...)same nameSDK index.js 3467Registers client-side handlers for incoming sessionFs.* reverse requests.
SDK provider adaptercreateSessionFsAdapter(...)same nameSDK index.js 4406Converts thrown provider errors into generated RPC result shapes.
SDK client startupprovider handshakesessionFs.setProvider requestSDK index.js 4833Sends initialCwd, sessionStatePath, and path conventions after connecting.
Per-session SDK handlercreateSessionFsHandlersame nameSDK index.js 5081, 5218Requires a provider per created/resumed session when client-level sessionFs is enabled.

Big picture

flowchart TD
Runtime["Session runtime"] --> Fs["SessionFs abstraction"]
Fs --> Local["Local VC backend\nDo.sessionFsLocal*"]
Fs --> Rpc["RPC i1t backend\nDo.sessionFsRpc*"]
Runtime --> Events["events.jsonl"]
Runtime --> Workspace["workspace.yaml / plan.md\ncheckpoints / files / research"]
Runtime --> Temp["large output temp files"]
Rpc --> Reverse["reverse-call token"]
Reverse --> Server["JSON-RPC server"]
Server --> Client["SDK client connection"]
Client --> Provider["SessionFsProvider implementation"]

The same session code writes event logs, workspaces, checkpoints, and temporary large-output files through a small filesystem interface. In normal CLI/TUI mode the backend is local. In SDK server mode a client can become the filesystem provider, so the CLI process stores all session-state files in a caller-controlled filesystem without rewriting the session manager.

Two backends

BackendCreated bysessionStatePathtmpdirTypical use
VC local backendnew VC(_y(sessionId, settings)) or VC.defaultOptional constructor argumentOS temp directoryTUI, prompt mode, server mode without custom provider.
i1t RPC backendsessionFs.setProvidersetSessionFsFactory(...)Required provider-config path<sessionStatePath>/tempSDK/server clients that want to own persistence.

Both extend Zge, which centralizes path joining and path separators. The local backend uses the host platform for path conventions. The RPC backend uses provider-supplied conventions (windows or posix) so the CLI can build paths that the remote/client filesystem understands.

The lock key differs by backend:

  • local VC.lockKey(path) returns the path itself;
  • RPC i1t.lockKey(path) prefixes the session id, producing ${sessionId}:${path}.

That distinction prevents cross-session lock collisions when multiple SDK sessions use the same provider connection and path style.

Local layout

When the default manager creates local session files, the path helper stack is:

I1(settings) -> <settings state root>/session-state
_y(sessionId, s) -> I1(s)/<sessionId>
Ip.path(id, s) -> I1(s)/<sessionId>/events.jsonl

The per-session workspace normally looks like:

session-state/<session-id>/
events.jsonl
workspace.yaml
plan.md
checkpoints/
index.md
001-<short-title>.md
files/
<persistent workspace artifacts>
research/
<research artifacts>
session.db

WorkspaceManager requires sessionStatePath. If a session is constructed with VC.default and no state path, the session can still execute, but workspace artifacts are unavailable and requireWorkspaceManager() throws.

SDK/RPC provider handshake

The SDK exposes a client-level sessionFs option:

FieldMeaning
initialCwdWorking directory returned by SessionFs.getInitialCwd() when a session does not pass an explicit working directory.
sessionStatePathRoot path inside each session’s provider-owned filesystem where the CLI stores session-scoped files.
conventionsPath conventions: windows or posix.

During SDK client startup, after JSON-RPC connection and protocol verification, the SDK sends:

sessionFs.setProvider({ initialCwd, sessionStatePath, conventions })

The server handler enforces three invariants:

  1. The provider must be installed before any sessions are active.
  2. Only one connection can be the session filesystem provider for the server process.
  3. If the provider connection disconnects, the runtime logs that the instance should not be reused.

When accepted, the handler creates a SessionFs factory that returns new i1t(clientConnection, sessionId, initialCwd, sessionStatePath, conventions) for every session id, then installs it through SessionManager.setSessionFsFactory(...).

Reverse-call path

The RPC backend does not call JSON-RPC directly from every method. Instead, it goes through native bridge helpers such as Do.sessionFsRpcReadFile(...) with a reverseCallHandler callback. The bridge produces a tokenized reverse-call request, and i1t.handleReverseCall(...) completes the token when the SDK client returns.

sequenceDiagram
autonumber
participant Runtime as Session runtime
participant Fs as RpcSessionFs i1t
participant Native as Do.sessionFsRpc*
participant Server as CLI JSON-RPC server
participant Sdk as SDK connection
participant Provider as SessionFsProvider
Runtime->>Fs: readFile(path)
Fs->>Native: sessionFsRpcReadFile(sessionId, path, reverseCallHandler)
Native-->>Fs: reverse-call token + method + params
Fs->>Sdk: sessionFs.readFile({ sessionId, path })
Sdk->>Sdk: registerClientSessionApiHandlers lookup
Sdk->>Provider: provider.readFile(path)
Provider-->>Sdk: content or throws
Sdk-->>Fs: generated SessionFs result
Fs->>Native: sessionFsCompleteReverseCall(token, success, json)
Native-->>Fs: result
Fs-->>Runtime: content or throws mapped error

The method switch in i1t.invokeClientSessionFs(...) recognizes exactly these client-session methods:

  • readFile
  • writeFile
  • appendFile
  • exists
  • stat
  • mkdir
  • readdir
  • readdirWithTypes
  • rm
  • rename

Unknown methods fail the reverse call. A reverse call for a different session id also fails, which protects a provider from accidentally serving the wrong session.

SDK-side provider contract

The SDK’s SessionFsProvider interface is idiomatic TypeScript: provider methods return values directly and throw on failure. createSessionFsAdapter(...) converts that into the generated RPC handler contract.

Provider behaviorAdapter behavior
readFile returns a stringReturns { content }.
writeFile / appendFile / mkdir / rm / rename succeedReturns undefined.
exists succeedsReturns { exists }.
stat succeedsReturns file metadata directly.
readdir succeedsReturns { entries }.
provider throws with code === "ENOENT"Maps to { code: "ENOENT", message }.
provider throws anything elseMaps to { code: "UNKNOWN", message }.

When client-level sessionFs is configured, every createSession(...) and resumeSession(...) call must provide createSessionFsHandler(session). The SDK stores the session before issuing session.create or session.resume, attaches session.clientSessionApis.sessionFs, and the globally registered client-session handlers route reverse calls by sessionId.

This split is subtle but important:

  • CopilotClientOptions.sessionFs declares a process-wide provider configuration and causes the server to switch filesystem factories.
  • SessionConfig.createSessionFsHandler supplies the actual provider implementation for each session object.

Event persistence through SessionFs

Ip.load(sessionFs) and Ip.append(events, sessionFs) are filesystem-agnostic. They build <sessionStatePath>/events.jsonl with sessionFs.join(...) and protect read/write operations with sessionFs.lockKey(...).

OperationLocal backendRPC backend
loadOpens a native line stream with Do.sessionFsLocalOpenReadFileStream(...).Reads the whole file and splits by line in i1t.readFileStream(...).
appendAppends newline-delimited JSON through Do.sessionFsLocalAppendFile(...).Reverse-calls provider appendFile(...).
truncateRewrites events.jsonl through Do.sessionFsLocalWriteFile(...).Reverse-calls provider writeFile(...).
metadata/listingUses local stat/readdir helpers.Reverse-calls provider stat/readdir operations.

ETt is the debounced writer that feeds Ip.append(...). Because it writes through session.sessionFs, the same persistence code works for local and SDK-provided storage.

Workspace artifacts through SessionFs

WorkspaceManager stores non-event artifacts in the same sessionStatePath:

PathProducer/consumer
workspace.yamlSession metadata, cwd, git root, repository, branch, friendly name, remote-state markers.
plan.mdPlan-mode content exposed through session plan APIs.
checkpoints/index.md and numbered markdown summariesCompaction checkpoints, /session checkpoints, rewind/fork naming, and checkpoint truncation.
files/Persistent session files and large pasted content.
research/Research/session-local artifacts.
session.dbSession-local database path for state that benefits from SQLite.

All reads/writes go through sessionFs. The path sanitizer for workspace files resolves paths under files/ and rejects attempts to escape that directory.

Large output and temporary files

Tool and shell output buffers also use SessionFs:

  • generic large tool results call LW(sessionFs, largeOutputConfig, grepToolName);
  • shell buffers create BW(...) with sessionFs in both PTY and process backends;
  • temp output paths are registered for cleanup with rAe(sessionFs, filePath);
  • cleanup later removes temp files through the same backend, unless COPILOT_KEEP_TEMP_FILES=true.

For the RPC backend, temp files live under <sessionStatePath>/temp, not the server’s OS temp directory. That keeps SDK-owned state inside the provider-controlled filesystem.

Forking and state copying

SessionManager.forkSession(...) is the densest state-file path.

flowchart TD
Fork["forkSession(source, toEventId?)"] --> Flush["flush source writer"]
Flush --> SourceFs["sessionFsFactory(sourceId)"]
SourceFs --> Events["Ip.load(sourceFs) or in-memory events"]
Events --> Slice["optional event boundary slice"]
Slice --> DestFs["sessionFsFactory(newId)"]
DestFs --> Rewrite["rewrite compaction checkpoint paths"]
Rewrite --> Copy["copy workspace metadata\nplan/checkpoints/files/research"]
Copy --> Truncate["truncate checkpoint summaries\nwhen bounded by event"]
Truncate --> Append["Ip.append(forked events, destFs)"]

Key fork behaviors:

  1. The source writer is flushed before reading persisted events.
  2. If no persisted events exist, the manager can fall back to in-memory non-ephemeral events.
  3. The first session.start event is rewritten with the new session id and start time.
  4. Compaction checkpoint paths inside events are rewritten from the source sessionStatePath prefix to the destination prefix.
  5. workspace.yaml is cloned with remote/Mission Control fields removed and the new id/name/timestamps applied.
  6. plan.md, checkpoints/, files/, and research/ are copied recursively when both source and destination filesystems expose sessionStatePath.
  7. If the fork is bounded by toEventId, checkpoint summaries after the last included compaction checkpoint are removed.
  8. A fork-info session.info event is appended to the new session and, when possible, also appended to the source session.

Because copy uses the abstract SessionFs operations (stat, readdirWithTypes, readFile, writeFile, mkdir), local-to-local and provider-backed copies use the same code path as long as both session ids use the same installed factory.

Operational guardrails

GuardrailWhy it matters
Install provider before sessions existPrevents one process from mixing local and provider-backed sessions.
One provider connection per server instancePrevents ambiguous reverse-call routing.
createSessionFsHandler required per sessionThe SDK needs a concrete handler for each sessionId before the runtime can read/write.
Provider disconnection is effectively terminalThe server logs that the runtime should not be reused because future persistence calls cannot be completed reliably.
Path conventions are explicitThe runtime uses provider conventions for joins and path separators instead of assuming the CLI host OS.
Provider errors are result-shapedcreateSessionFsAdapter(...) maps thrown errors into generated RPC results, and the runtime rethrows them with Fg(...).

Relationship to other pages

Created and maintained by Yingting Huang.