session-lifecycle
Skill9 starsChanged 17 days agoApache-2.0View on GitHub
ShipIt session lifecycle: session types (standalone, repo-backed, warm), creation paths, warm session pool mechanics, session activation flow, session switching. Load when working on session creation, warm pool, or activation logic.
3 other projects use a version of this.
Adopted by
Most recently taken up by
- blamechris/chroxytheir own version, first seen Nov 8, 2026
- Kangentic/kangentictheir own version, first seen Nov 7, 2026
- Giobebbe/second-brain-os-templatetheir own version, first seen Apr 11, 2026
Other versions3 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 3
- Kangentic/kangentic★ 140same name
- Giobebbe/second-brain-os-template★ 9same name
- blamechris/chroxy★ 8same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository15 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- add-endpointSkill
- android-buildSkill
- client-architectureSkill
- deployment-architectureSkill
- design-languageSkill
- docs-navigatorSkill
- dogfooding-shipitSkill
- frontend-design:frontend-designSkill
- git-architectureSkill
- prompt-architectureSkill
- server-architectureSkill
- session-containersSkill
- session-processesSkill
- testing-and-qualitySkill
The file
We split this skill into the rules it gives an agent
2 rules. 0 are common, 0 are distinctive, and 2 are very specific to this project.
0
Common
Most of the 3 edited versions we read keep these.
0
Distinctive
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 93
If the client disconnected before work starts (request.raw.destroyed), the endpoint short-circuits to avoid creating abandoned sessions.
- Very specific: line 202
When the user sends their first message on a warm session, handleSendMessage "graduates" it:
“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Session Lifecycle
This skill covers session creation, warm-up, activation, switching, and graduation. For container/runner infrastructure, see the session-containers skill.
Key Components
| Component | Location | Role |
SessionManager | orchestrator/sessions.ts | Persists session metadata (title, workspace dir, remote URL, warm flag) in the SQLite sessions table |
SessionRunnerRegistry | orchestrator/session-runner.ts | App-level map of session ID -> runner. Fires onRunnerIdle callback for container cleanup |
SessionRunnerInterface | orchestrator/session-runner.ts | Abstract contract: agent state, message queue, viewer count, preview |
RepoStore | orchestrator/repo-store.ts | Tracks imported repos, clone status, warm session IDs |
Session Types
- Standalone session — no repo, fresh git repo initialized in the session directory. Created via
POST /api/sessions. - Repo-backed session — its own independent local clone cut from the per-remote bare cache (
git clone --local, hardlinked objects), checked out on a unique branch off the default branch. Created via warm pool orclaim-session. - Warm session — a repo-backed session pre-created in the background: clone-from-cache + metadata plus, where there is idle headroom, a pre-booted standby container. Invisible in the sidebar until the user sends their first message ("graduated"). Where no standby exists, the container is created on-demand when the WebSocket connects. A warm session never survives an orchestrator restart — see "Restart" below.
Session Creation
Path A: Standalone Session (no repo)
Client Server
| |
+- POST /api/sessions ---------> createSessionDir(title)
| {title} | mkdir sessions/{uuid}
| | git init
| | configure credentials
| | sessionManager.track()
|<- {sessionId, sessionDir} -----
| |
| store pendingWsMessage |
| navigate(/session/{id}) |
| |
| useSessionWebSocket opens |
+- WS /ws/sessions/{id} -------> activateSession(id)
| | runnerRegistry.getOrCreate()
| | -> factory creates container
| | attachToRunner()
| |
| useConnectionSync fires |
+- GET /api/sessions/{id}/history -> returns messages, commits, etc.
| |
| send pendingWsMessage |
+- WS send_message ------------> handleSendMessage()
| | POST /agent/start to worker
|<- WS streaming events ---------Path B: Warm Session (repo, pool hit)
The claim endpoint first checks for a reusable session: the never-graduated draft the server last gave to the same browser tab (the client sends a per-page-load tabId; claim-session.ts keeps a tab → draft map). If that draft is still warm and carries no settings, it returns it — reusing the existing container instead of creating a new one. No other draft is reused, so two tabs never share one (docs/285-network-mode-at-session-creation req 13). Otherwise, it claims from the warm pool.
Client Server
| |
| navigate(/{owner}/{repo}/new)|
+- POST /api/repos/:url/claim-session ->
| | 1. this tab's own draft, still reusable?
| | YES -> return it (no new claim)
| | 2. repo.warmSessionId exists?
| | YES -> clear warmSessionId
| | refreshCloneToLatestMain (git fetch)
| | re-warm next session (FIRE-AND-FORGET,
| | with standby -- not awaited; docs/144)
|<- {sessionId, fetchDurationMs}- (returns after refresh; re-warm runs async)
| |
| store pendingWsMessage |
| navigate(/session/{id}) |
| |
| WS /ws/sessions/{id} -------> activateSession(id)
| | runnerRegistry.getOrCreate()
| | -> claim standby (instant) or create
| | attachToRunner()
|<- WS preview_status ---------- (instant if standby; else once it boots)
| |
| send pendingWsMessage |
+- WS send_message ------------> graduates warm session
| | (rename, clear warm flag, broadcast)Path C: Claim Session (repo, no warm pool)
Same as Path B but claim-session creates the session clone synchronously (~1-2s).
If the client disconnected before work starts (request.raw.destroyed), the
endpoint short-circuits to avoid creating abandoned sessions.
createSessionDirFull("Warm session", { warm: true })— the row is inserted warm, so no session list shows it during the clone. Spawned children and scheduled runs always take this path (skipWarm), so a row inserted visible showed in the sidebar as "Warm session" for the whole clone.- Clone from bare cache + fetch real remote +
checkout -b(branch from latest) - Configure credentials
- Fire-and-forget re-warm of the next session (clone + standby + pre-install)
- Return session ID +
fetchDurationMsto client (no container created on this path)
The container is created on-demand when the WebSocket connects (activateSession
-> getOrCreate -> factory creates container), since the slow path had no
standby to claim.
The client passes an AbortSignal to the claim fetch. Navigating away (clicking
another session or "New Session" again) aborts the request, which the server
detects via request.raw.destroyed.
Warm Session Pool
The warm pool pre-creates one session per repo so users get instant "New Session" with a running preview.
Two mechanisms prevent cascade during rapid "New Session" clicks:
warmingInProgressset (per repo URL) prevents concurrentwarmSessionForRepocalls. Without this, each click triggers a replacement warm, and while those are in-flight (beforewarmSessionIdis set), subsequent clicks see no warm session and fall to the slow path.warmingPromisesmap stores the in-flight warming promise. When the claim endpoint finds no warm session but warming IS in progress, it awaits the promise and re-checks — claiming the freshly created warm session instead of falling to the expensive slow path.
Warm-Up Sequence
Warm-up creates the session clone + metadata and pre-boots a standby
container (containerManager.createStandby), on which it then pre-runs
agent.install. The runner factory claimStandbys that container on
activation, so a warm hit reconnects to an already-running, already-installed
worker instead of building one — see the session-containers skill.
warmSessionForRepo(repoUrl):
1. Check repo.status === "ready", no existing warm session, no warming in progress
2. createSessionDir("Warm session", { warm: true }) (row inserted warm)
3. sessionManager.setRemoteUrl(...)
4. Remove workspace subdir (clone needs it absent)
5. cloneFromCache + fetchAndResolveDefaultBranch + checkout -b (real-remote
fetch so the branch is cut from genuine latest — see W2)
6. Configure git credentials; materialize LFS; hand the tree back to the worker uid
7. repoStore.setWarmSessionId(repoUrl, appSessionId)
8. createStandby(...) + runPreInstall(...) [fire-and-forget], gated on: the OOM
breaker not tripped, memory headroom (not at the docs/284 memory budget), and —
for the pre-install only — the remote being trusted (docs/178)
9. sseBroadcast("repo_warm_ready", ...)There is deliberately no caller-side opt-out: every path that warms a repo
wants the standby and the pre-install, and a "warm but no standby" state is what
made docs/148 silently regress. A runtime with no containers expresses that by
passing containerManager: null. Callers are the initial warm of a freshly
added repo, the startup re-warm, the claim re-warm (claim-session) and
graduation (send-message). The claim re-warm is fire-and-forget (not
awaited) so it never sits on the claiming user's critical path — see
docs/144-session-switch-latency.
Restart: Warm Sessions Do Not Survive It
A standby belongs to the process that created it. It holds no work and runs that
process's worker image, and nothing else would ever reap one — the idle enforcer
skips standbys deliberately, and rediscoverContainers re-adopts them standby
flag and all. So a standby used to outlive every deploy, and the next claim
handed the user a grandfathered worker carrying a pre-install and an overlay base
built under the image the deploy replaced. (Grandfathering a real session's
container across a deploy is the opposite call and it stands — docs/113 exists to
protect work in flight, which a standby has none of.)
Boot therefore retires the whole warm tier, in this order:
retireWarmSessions(startup-tasks.ts), called frombootstrapManagersbeforesetupContainerManager: deletes everywarm = 1row — the pool session a repo points at, and any claimed-but-never-graduated draft — clears each repo'swarmSessionId, and reclaims the clone + overlay (reclaimRegenerableSessionDirs, which preservesuploads/). The ordering is the point: with the rows gone, the container sweeps on the next line treat each standby as an orphan and rediscovery cannot re-adopt one.reapStandbyContainers(activeSessionIds)(container-discovery.ts), called fromsetupContainerManager: stops and removes everyshipit-standby=truecontainer whose session is no longer tracked. It runs outside the Docker-available branch so an injected container manager gets the same guarantee.The live-session set is not a refinement — it is what stops the sweep destroying live sessions. The label is set at create time and Docker cannot change one afterwards, so
claimStandbyonly drops the in-process flag: a claimed, graduated session keeps ashipit-standby=truecontainer for that container's whole life. Label means "was born a standby", never "is one now"; the session row is what separates them, cleanly, because step 1 has already deleted every warm row. For the same reasonrediscoverContainersdoes not restore the standby flag from the label.- Re-warm:
scheduleStartupTasks(deferred viasetTimeout(0)) then finds every ready repo without awarmSessionIdand callswarmSessionForRepo, rebuilding the pool — fresh clone, fresh standby, fresh pre-install — on the new image. Its zombie sweep and stale-clone validation loop normally find nothing now; they remain for the rows retirement cannot recognise (a zombie whose warm flag was already cleared, identifiable only by its title).
Guards: startup-tasks.test.ts, container-discovery.test.ts,
integration_tests/standby-container.test.ts.
Graduation
When the user sends their first message on a warm session, handleSendMessage "graduates" it:
- Clears the
warmflag - Renames from "Warm session" to a meaningful title
- Broadcasts
session_listupdate (session appears in sidebar)
Session Activation (Per-Session WebSocket)
The client connects to ws[s]://host/ws/sessions/{sessionId}?agent=claude. The server handles this in buildApp():
Server-Side (on WS connect)
1. Validate session exists (close 4004 if not)
2. Initialize per-connection state:
- activeAppSessionId = sessionId
- activeSessionDir = session.workspaceDir
- perConnectionAgentId = query.agent ?? defaultAgentId
3. activateSession(sessionId):
a. existingRunner = runnerRegistry.get(sessionId)
b. If exists: attachToRunner(existingRunner)
c. Else: runner = runnerRegistry.getOrCreate(sessionId, dir, agentId)
attachToRunner(runner)
4. Send log buffer
5. Re-send preview_status after log buffer (React batching mitigation)attachToRunner(runner)
1. Detach from any previous runner
2. Subscribe runner.on("message", send)
3. runner.attachViewer() -- increments viewer count
4. Replay turn event buffer (all events from current turn)
5. Send queue status if messages are queued
6. Send session_status if agent is running
7. Send preview_status ONLY IF runner.previewStatusKnown === true
(otherwise, the runner will emit it later when SSE delivers state)
8. If previewStatusKnown === false:
Register a one-shot "message" listener on the runner. When the first
preview_status arrives, re-send it in a separate microtask (queueMicrotask)
so it survives React 18 batching where rapid setLastMessage calls drop
intermediate messages.Client-Side (on WS open)
useConnectionSync:
1. GET /api/sessions/{id}/history -> messages, commits, fileTree, threads
2. GET /api/sessions/{id}/preview-status -> HTTP fallback for preview state
(only applied if store still has preview=null)
If response is known: false -> retry once after 3s (by then SSE has connected)
3. Send pendingWsMessage if presentSession Switching (Client-Side)
Switching to an Existing Session
handleSessionResume(sessionId, navigate):
1. resumeSessionInternal(sessionId):
a. Set sessionId in session store
b. Clear messages, loading, queue
c. Reset all session-specific stores (files, git, threads, terminal, UI, preview)
-> preview store reset to null
d. loadSessionHistory(sessionId) via HTTP (async)
2. navigate("/session/{sessionId}")
-> URL change triggers React re-render
-> useSessionWebSocket computes new WS URL
-> useWebSocket closes old WS, opens new WSServer-side effects:
Old WS close:
-> socket.on("close") fires
-> detachFromRunner() -- decrements viewer count on old runner
New WS open:
-> activateSession(newSessionId)
-> getOrCreate() gets (or creates) runner
-> attachToRunner() -- subscribes to events, replays stateCreating a New Session from Home
newSession(navigate):
1. Clear sessionId
2. Reset all session state
3. Show templates
4. navigate("/")
-> WS disconnects when leaving /session/* URLDoes it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

