Skip to main content
Glama

session_save_handoff

Save project context for the next session by updating handoff state with open todos, active branch, and session summary to enable seamless continuation.

Instructions

Upsert the latest project handoff state for the next session to consume on boot. This is the 'live context' that gets loaded when a new session starts. Calling this replaces the previous handoff for the same project (upsert on project).

v0.4.0 OCC: If you received a version number from session_load_context, /resume_session prompt, or memory resource attachment, you MUST pass it as expected_version to prevent overwriting another session's changes.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
projectYesProject identifier — must match the project used in session_save_ledger.
expected_versionNov0.4.0: The version number you received when loading context. Pass this to enable optimistic concurrency control. If omitted, version check is skipped (backward compatible).
open_todosNoCurrent open TODO items that need attention in the next session.
active_branchNoGit branch or context the next session should resume on.
last_summaryNoSummary of the most recent session — used for quick context recovery.
key_contextNoFree-form critical context the next session needs to know.

Implementation Reference

  • The handler function 'sessionSaveHandoffHandler' which manages the saving of project handoff states, handles version conflicts (OCC), and triggers background tasks like history snapshotting, resource notification, sync broadcast, auto-capture, and fact merging.
    export async function sessionSaveHandoffHandler(args: unknown, server?: Server) {
      if (!isSessionSaveHandoffArgs(args)) {
        throw new Error("Invalid arguments for session_save_handoff");
      }
    
      const {
        project,
        expected_version,
        open_todos,
        active_branch,
        last_summary,
        key_context,
      } = args;
    
      const storage = await getStorage();
    
      console.error(
        `[session_save_handoff] Saving handoff for project="${project}" ` +
        `(expected_version=${expected_version ?? "none"})`
      );
    
      // Auto-extract keywords from summary + context for knowledge accumulation
      const combinedText = [last_summary || "", key_context || ""].filter(Boolean).join(" ");
      const keywords = combinedText ? toKeywordArray(combinedText) : undefined;
      if (keywords) {
        console.error(`[session_save_handoff] Extracted ${keywords.length} keywords: ${keywords.slice(0, 5).join(", ")}...`);
      }
    
      // Auto-capture Git state for Reality Drift Detection (v2.0 Step 5)
      const gitState = getCurrentGitState();
      const metadata: Record<string, unknown> = {};
      if (gitState.isRepo) {
        metadata.git_branch = gitState.branch;
        metadata.last_commit_sha = gitState.commitSha;
        console.error(
          `[session_save_handoff] Git state captured: branch=${gitState.branch}, sha=${gitState.commitSha?.substring(0, 8)}`
        );
      }
    
      // Save via storage backend (OCC-aware)
      const data = await storage.saveHandoff(
        {
          project,
          user_id: PRISM_USER_ID,
          last_summary: last_summary ?? null,
          pending_todo: open_todos ?? null,
          active_decisions: null,
          keywords: keywords ?? null,
          key_context: key_context ?? null,
          active_branch: active_branch ?? null,
          metadata,
        },
        expected_version ?? null
      );
    
      // ─── Handle version conflict ───
      if (data.status === "conflict") {
        console.error(
          `[session_save_handoff] VERSION CONFLICT for "${project}": ` +
          `expected=${expected_version}, current=${data.current_version}`
        );
    
        return {
          content: [{
            type: "text",
            text: `⚠️ Version conflict detected for project "${project}"!\n\n` +
              `You sent version ${expected_version}, but the current version is ${data.current_version}.\n` +
              `Another session has updated this project since you loaded context.\n\n` +
              `Please call session_load_context to see what changed, then merge ` +
              `it with your attempted updates:\n` +
              (last_summary
                ? `  Your attempted summary: ${last_summary}\n`
                : "") +
              (open_todos?.length
                ? `  Your attempted TODOs: ${JSON.stringify(open_todos)}\n`
                : "") +
              (key_context
                ? `  Your attempted key_context: ${key_context}\n`
                : "") +
              (active_branch
                ? `  Your attempted active_branch: ${active_branch}\n`
                : "") +
              `\nAfter reviewing the latest state, call session_save_handoff again ` +
              `with the updated expected_version.`,
          }],
          isError: true,
        };
      }
    
      // ─── Success: handoff created or updated ───
      const newVersion = data.version;
    
      // ─── TIME MACHINE: Auto-snapshot for time travel (fire-and-forget) ───
      // Every successful save creates a snapshot so the user can revert later.
      // We don't await — this should never block the success response.
      if (data.status === "created" || data.status === "updated") {
        const snapshotEntry = {
          project,
          user_id: PRISM_USER_ID,
          last_summary: last_summary ?? null,
          pending_todo: open_todos ?? null,
          active_decisions: null,
          keywords: keywords ?? null,
          key_context: key_context ?? null,
          active_branch: active_branch ?? null,
          version: newVersion,
        };
        storage.saveHistorySnapshot(snapshotEntry).catch(err =>
          console.error(`[session_save_handoff] History snapshot failed (non-fatal): ${err}`)
        );
      }
    
      // ─── Trigger resource subscription notification ───
      if (server && (data.status === "created" || data.status === "updated")) {
        try {
          notifyResourceUpdate(project, server);
        } catch (err) {
          console.error(`[session_save_handoff] Resource notification failed (non-fatal): ${err}`);
        }
      }
    
      // ─── TELEPATHY: Broadcast to other Prism MCP instances (v2.0 Step 6) ───
      if (data.status === "created" || data.status === "updated") {
        import("../sync/factory.js")
          .then(({ getSyncBus }) => getSyncBus())
          .then(bus => bus.broadcastUpdate(project, newVersion ?? 1))
          .catch(err =>
            console.error(`[session_save_handoff] SyncBus broadcast failed (non-fatal): ${err}`)
          );
      }
    
      // ─── AUTO-CAPTURE: Snapshot local dev server HTML (v2.1 Step 10) ───
      // Fire-and-forget — never blocks the handoff response.
      if (PRISM_AUTO_CAPTURE && (data.status === "created" || data.status === "updated")) {
        captureLocalEnvironment(project, PRISM_CAPTURE_PORTS).then(async (captureMeta) => {
          if (captureMeta) {
            try {
              const latestCtx = await storage.loadContext(project, "quick", PRISM_USER_ID);
              if (latestCtx) {
                const ctx = latestCtx as any;
                const updatedMeta = { ...(ctx.metadata || {}) };
                updatedMeta.visual_memory = updatedMeta.visual_memory || [];
                updatedMeta.visual_memory.push(captureMeta);
    
                await storage.saveHandoff({
                  project,
                  user_id: PRISM_USER_ID,
                  metadata: updatedMeta,
                  last_summary: ctx.last_summary ?? null,
                  pending_todo: ctx.pending_todo ?? null,
                  active_decisions: ctx.active_decisions ?? null,
                  keywords: ctx.keywords ?? null,
                  key_context: ctx.key_context ?? null,
                  active_branch: ctx.active_branch ?? null,
                }, newVersion);
                console.error(`[AutoCapture] HTML snapshot indexed in visual memory for "${project}"`);
              }
            } catch (err) {
              console.error(`[AutoCapture] Metadata patch failed (non-fatal): ${err}`);
            }
          }
        }).catch(err => console.error(`[AutoCapture] Background task failed (non-fatal): ${err}`));
      }
    
      // ─── FACT MERGER: Async LLM contradiction resolution (v2.3.0) ───
      // Fire-and-forget — the agent gets instant "✅ Saved" while Gemini
      // merges contradicting facts in the background (~2-3s).
      //
      // TRIGGER CONDITIONS (all must be true):
      //   1. GOOGLE_API_KEY is configured (Gemini is available)
      //   2. The handoff was an UPDATE (not a brand-new project)
      //   3. key_context was provided (something to merge)
      //
      // OCC SAFETY:
      //   If the user saves another handoff while the merger runs,
      //   the merger's save will fail with a version conflict. This is
      //   intentional — active user input always wins over background merging.
      if (GOOGLE_API_KEY && data.status === "updated" && key_context) {
        // Use dynamic import to avoid loading Gemini SDK if not needed
        import("../utils/factMerger.js").then(async ({ consolidateFacts }) => {
          try {
            // Step 1: Load the old context from the database
            const oldState = await storage.loadContext(project, "quick", PRISM_USER_ID);
            const oldKeyContext = (oldState as any)?.key_context || "";  // extract old key_context
    
            // Step 2: Skip merge if old context is empty (nothing to merge with)
            if (!oldKeyContext || oldKeyContext.trim().length === 0) {
              console.error("[FactMerger] No old context to merge — skipping");
              return;  // first handoff for this project, no merge needed
            }
    
            // Step 3: Call Gemini to intelligently merge old + new context
            const mergedContext = await consolidateFacts(oldKeyContext, key_context);
    
            // Step 4: Skip patch if merged result is same as current key_context
            if (mergedContext === key_context) {
              console.error("[FactMerger] No changes after merge — skipping patch");
              return;  // Gemini determined no contradictions existed
            }
    
            // Step 5: Silently patch the database with the merged context
            // Uses the current version for OCC — if user saved again, this will
            // fail with a version conflict (which is the correct behavior)
            await storage.saveHandoff({
              project,                                // same project
              user_id: PRISM_USER_ID,                 // same user
              key_context: mergedContext,              // merged context (cleaned by Gemini)
              last_summary: last_summary ?? null,      // preserve existing summary
              pending_todo: open_todos ?? null,        // preserve existing TODOs
              active_decisions: null,                  // preserve existing decisions
              keywords: keywords ?? null,              // preserve existing keywords
              active_branch: active_branch ?? null,    // preserve existing branch
              metadata: {},                            // no metadata changes
            }, newVersion);                            // use current version for OCC
    
            console.error("[FactMerger] Context merged and patched for \"" + project + "\"");
          } catch (err) {
            // OCC conflict = user saved again while merge was running (expected)
            const errMsg = err instanceof Error ? err.message : String(err);
            if (errMsg.includes("conflict") || errMsg.includes("version")) {
              // This is GOOD behavior — user's active input takes precedence
              console.error("[FactMerger] Merge skipped due to active session (OCC conflict)");
            } else {
              // Unexpected error — log but don't crash
              console.error("[FactMerger] Background merge failed (non-fatal): " + errMsg);
            }
          }
        }).catch(err =>
          // Dynamic import itself failed — module not found or similar
          console.error("[FactMerger] Module load failed (non-fatal): " + err)
        );
      }
    
      return {
        content: [{
          type: "text",
          text: `✅ Handoff ${data.status || "saved"} for project "${project}" ` +
            `(version: ${newVersion})\n` +
            (last_summary ? `Last summary: ${last_summary}\n` : "") +
            (open_todos?.length ? `Open TODOs: ${open_todos.length} items\n` : "") +
            (active_branch ? `Active branch: ${active_branch}\n` : "") +
            `\n🔑 Remember: pass expected_version: ${newVersion} on your next save ` +
            `to maintain concurrency control.`,
        }],
        isError: false,
      };
    }
  • Validation function 'isSessionSaveHandoffArgs' ensuring the correct structure for 'session_save_handoff' tool inputs.
    export function isSessionSaveHandoffArgs(
      args: unknown
    ): args is {
      project: string;
      expected_version?: number;
      open_todos?: string[];
  • src/server.ts:558-563 (registration)
    Tool handler registration in the main server for 'session_save_handoff'.
    case "session_save_handoff":
      if (!SESSION_MEMORY_ENABLED) throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
      // REVIEWER NOTE: v0.4.0 passes the server reference so the
      // handler can trigger resource update notifications after
      // a successful save. See notifyResourceUpdate() above.
      return await sessionSaveHandoffHandler(args, server);

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dcostenco/BCBA'

If you have feedback or need assistance with the MCP directory API, please join our Discord server