startSession
uses node:crypto
Claim the branch a session's server (this process) works on, and say so in its record: named (tcm mcp --branch, made from the codebase's own if it's new), or else the branch of this client's latest session (one it
made, or moved onto: its lease, leaseFile), if it
holds work main doesn't have (or a scratch file holding unsaved work) and no other server works on it (so a server
started again, after a crash or a client reconnecting, goes on with it), or else a new one (session/<date>-<id>).
It's claimed in a write transaction, as endSession and moveSession decide in one, so two servers never share a
branch and none is deleted under its server. A branch another server of the same client holds (one being replaced
that hasn't stopped yet) is waited for, wait milliseconds at most; a named one another client's server holds is
refused.
import { sessionDirFor, unsavedIn } from "#codebase/tcmts/cli/codebase"; import { liveMcpServers, readLease, withCodebase, writeLease, writeServerRecord } from "#codebase/tcmts/cli/mcp"; import { hasWork } from "#codebase/tcmts/cli/merge"; import { randomBytes } from "node:crypto"; export async function startSession( root: string, client: string, named?: string, wait = 3000, ): Promise<{ branch: string; resumed: boolean }> { const until = Date.now() + wait; for (;;) { const claimed = withCodebase(root, (cb) => cb.store.transaction(() => { const others = liveMcpServers(root).filter((s) => s.pid !== process.pid); const holder = (b: string) => others.find((s) => s.branch === b); const own = cb.store.branch().name; let branch: string | undefined; let resumed = false; if (named) { const on = holder(named); if (on && on.client === client && Date.now() < until) return undefined; if (on) throw new Error( `a session's server (process ${on.pid}) works on ${named}: two servers don't share a branch (stop that one first, or start this one without --branch)`, ); if (!cb.store.hasBranch(named)) cb.store.createBranch(named); branch = named; } else { const earlier = cb.store .branches() .filter((b) => b !== own) .flatMap((b) => { const lease = readLease(root, b); return lease?.client === client ? [{ b, at: lease.at }] : []; }) .sort((x, y) => y.at - x.at); for (const { b } of earlier) { const work = unsavedIn(sessionDirFor(root, b)).length > 0 || hasWork(cb, b, own); if (!work) continue; const on = holder(b); if (on && on.client === client && Date.now() < until) return undefined; if (on) continue; branch = b; resumed = true; break; } if (!branch) { branch = `session/${new Date().toISOString().slice(0, 10)}-${randomBytes(3).toString("hex")}`; cb.store.createBranch(branch); } } writeServerRecord(root, { pid: process.pid, branch, client }); // (a named one is the client's to name again: its lease stays as it was) if (!named) writeLease(root, branch, client); return { branch, resumed }; }), ); if (claimed) return claimed; await new Promise((r) => setTimeout(r, 200)); } }
uses
- tcmts.cli.codebase.sessionDirFor
- tcmts.cli.codebase.unsavedIn
- tcmts.cli.mcp.liveMcpServers
- tcmts.cli.mcp.readLease
- tcmts.cli.mcp.withCodebase
- tcmts.cli.mcp.writeLease
- tcmts.cli.mcp.writeServerRecord
- tcmts.cli.merge.hasWork