startSession

tcmts.cli.mcp.startSession · #la6aztlm · term · runs on the server

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

used by