board_create_session
Start a new work session on a project. Automatically abandons previous active sessions and returns a handoff with last session's progress, active tasks, and recent activity for context.
Instructions
Start a new work session on a project and get the previous session's handoff. Side effect: any currently-active sessions on the same project are automatically marked 'abandoned' with ended_at=now — there's only ever one active session per project. Call this at the start of every substantive session so the next one can pick up where you left off. The returned handoff includes: last_session (progress_summary + handoff_notes + context_artifacts from the previous run), active_tasks (priority-sorted non-done tasks), and recent_activity (last 20 activity_log entries). Returns { session_id, abandoned_sessions, handoff, message }.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| project_id | Yes | Project ID (from board_get_projects) the session operates on | |
| session_type | No | Session type. 'solo' (default) = single agent, 'team' = coordinated multi-agent, 'background' = long-running async work like a Docker worker. | |
| metadata | No | Optional metadata (e.g., { worker_id: 'batch-123', hostname: 'mig-5' }). Stored on the session document verbatim. |
Implementation Reference
- src/tools/sessions.ts:20-86 (handler)The async handler function that executes the board_create_session tool logic: abandons stale active sessions, creates a new session, logs activity, and builds handoff context.
async ({ project_id, session_type, metadata }) => { const now = Timestamp.now(); // 1. Abandon stale active sessions const activeSessions = await db .collection("sessions") .where("project_id", "==", project_id) .where("status", "==", "active") .get(); const batch = db.batch(); activeSessions.docs.forEach((doc) => { batch.update(doc.ref, { status: "abandoned", ended_at: now, progress_summary: doc.data().progress_summary ?? "Session abandoned (new session started)", }); }); // 2. Create new session const sessionRef = db.collection("sessions").doc(); batch.set(sessionRef, { project_id, session_type: session_type ?? "solo", status: "active", started_at: now, ended_at: null, progress_summary: null, handoff_notes: null, context_artifacts: {}, metadata: metadata ?? {}, }); await batch.commit(); // 3. Log activity await db.collection("activity_log").add({ task_id: null, session_id: sessionRef.id, agent_name: "system", action: "session_started", details: `New ${session_type ?? "solo"} session started${activeSessions.size > 0 ? ` (${activeSessions.size} stale session(s) abandoned)` : ""}`, metadata: {}, created_at: now, }); // 4. Build handoff context const handoff = await buildHandoffContext(db, project_id); return { content: [ { type: "text" as const, text: JSON.stringify( { session_id: sessionRef.id, abandoned_sessions: activeSessions.size, handoff: handoff, message: "Session started successfully", }, null, 2 ), }, ], }; } - src/tools/sessions.ts:9-19 (schema)Input schema for board_create_session using Zod: project_id (required string), session_type (optional enum), metadata (optional record).
{ project_id: z.string().describe("Project ID (from board_get_projects) the session operates on"), session_type: z .enum(["solo", "team", "background"]) .optional() .describe("Session type. 'solo' (default) = single agent, 'team' = coordinated multi-agent, 'background' = long-running async work like a Docker worker."), metadata: z .record(z.string(), z.unknown()) .optional() .describe("Optional metadata (e.g., { worker_id: 'batch-123', hostname: 'mig-5' }). Stored on the session document verbatim."), }, - src/tools/sessions.ts:5-87 (registration)The registerSessionTools function registers 'board_create_session' via server.tool() with name, description, schema, and handler.
export function registerSessionTools(server: McpServer, db: Firestore) { server.tool( "board_create_session", "Start a new work session on a project and get the previous session's handoff. **Side effect**: any currently-active sessions on the same project are automatically marked 'abandoned' with ended_at=now — there's only ever one active session per project. Call this at the start of every substantive session so the next one can pick up where you left off. The returned handoff includes: last_session (progress_summary + handoff_notes + context_artifacts from the previous run), active_tasks (priority-sorted non-done tasks), and recent_activity (last 20 activity_log entries). Returns { session_id, abandoned_sessions, handoff, message }.", { project_id: z.string().describe("Project ID (from board_get_projects) the session operates on"), session_type: z .enum(["solo", "team", "background"]) .optional() .describe("Session type. 'solo' (default) = single agent, 'team' = coordinated multi-agent, 'background' = long-running async work like a Docker worker."), metadata: z .record(z.string(), z.unknown()) .optional() .describe("Optional metadata (e.g., { worker_id: 'batch-123', hostname: 'mig-5' }). Stored on the session document verbatim."), }, async ({ project_id, session_type, metadata }) => { const now = Timestamp.now(); // 1. Abandon stale active sessions const activeSessions = await db .collection("sessions") .where("project_id", "==", project_id) .where("status", "==", "active") .get(); const batch = db.batch(); activeSessions.docs.forEach((doc) => { batch.update(doc.ref, { status: "abandoned", ended_at: now, progress_summary: doc.data().progress_summary ?? "Session abandoned (new session started)", }); }); // 2. Create new session const sessionRef = db.collection("sessions").doc(); batch.set(sessionRef, { project_id, session_type: session_type ?? "solo", status: "active", started_at: now, ended_at: null, progress_summary: null, handoff_notes: null, context_artifacts: {}, metadata: metadata ?? {}, }); await batch.commit(); // 3. Log activity await db.collection("activity_log").add({ task_id: null, session_id: sessionRef.id, agent_name: "system", action: "session_started", details: `New ${session_type ?? "solo"} session started${activeSessions.size > 0 ? ` (${activeSessions.size} stale session(s) abandoned)` : ""}`, metadata: {}, created_at: now, }); // 4. Build handoff context const handoff = await buildHandoffContext(db, project_id); return { content: [ { type: "text" as const, text: JSON.stringify( { session_id: sessionRef.id, abandoned_sessions: activeSessions.size, handoff: handoff, message: "Session started successfully", }, null, 2 ), }, ], }; } ); - src/tools/sessions.ts:186-284 (helper)buildHandoffContext helper function called by board_create_session handler - fetches project info, last session, active tasks (priority-sorted), and recent activity (last 20 entries).
async function buildHandoffContext(db: Firestore, project_id: string) { // Get project info const projectSnap = await db.collection("projects").doc(project_id).get(); const projectData = projectSnap.exists ? projectSnap.data() : null; // Get last completed or abandoned session const lastSessionSnap = await db .collection("sessions") .where("project_id", "==", project_id) .where("status", "in", ["completed", "abandoned"]) .orderBy("ended_at", "desc") .limit(1) .get(); const lastSession = lastSessionSnap.docs[0] ? (() => { const data = lastSessionSnap.docs[0].data(); return { id: lastSessionSnap.docs[0].id, status: data.status, progress_summary: data.progress_summary, handoff_notes: data.handoff_notes, context_artifacts: data.context_artifacts, started_at: data.started_at?.toDate?.()?.toISOString() ?? null, ended_at: data.ended_at?.toDate?.()?.toISOString() ?? null, }; })() : null; // Get all non-done tasks sorted by priority const tasksSnap = await db .collection("tasks") .where("project_id", "==", project_id) .where("status", "!=", "done") .get(); const priorityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3, }; const activeTasks = tasksSnap.docs .map((doc) => { const data = doc.data(); return { id: doc.id, title: data.title, status: data.status, priority: data.priority, assigned_agent: data.assigned_agent, riper_mode: data.riper_mode, depends_on: data.depends_on, }; }) .sort( (a, b) => (priorityOrder[a.priority] ?? 99) - (priorityOrder[b.priority] ?? 99) ); // Get recent activity const activitySnap = await db .collection("activity_log") .orderBy("created_at", "desc") .limit(20) .get(); // Filter to project-related activity (tasks in this project or sessions in this project) const taskIds = new Set(tasksSnap.docs.map((d) => d.id)); const recentActivity = activitySnap.docs .map((doc) => { const data = doc.data(); return { id: doc.id, action: data.action, agent_name: data.agent_name, details: data.details, task_id: data.task_id, session_id: data.session_id, created_at: data.created_at?.toDate?.()?.toISOString() ?? null, }; }); return { project: projectData ? { id: project_id, name: projectData.name, status: projectData.status, description: projectData.description, } : null, last_session: lastSession, active_tasks: activeTasks, active_task_count: activeTasks.length, recent_activity: recentActivity, }; }