Skip to main content
Glama

twining_archive

Archive old blackboard entries by moving entries before a cutoff timestamp to an archive file, preserving decisions and optionally posting a summary finding.

Instructions

Archive old blackboard entries. Moves entries older than a cutoff timestamp to an archive file, preserving decision entries. Optionally posts a summary finding.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
beforeNoISO timestamp cutoff — archive entries before this time (default: now)
keep_decisionsNoWhether to keep decision entries in the blackboard (default: true)
summarizeNoWhether to post a summary finding after archiving (default: true)

Implementation Reference

  • Tool registration of 'twining_archive' — registers the tool on the MCP server with schema for 'before', 'keep_decisions', and 'summarize' parameters.
    // twining_archive — Archive old blackboard entries
    server.registerTool(
      "twining_archive",
      {
        description:
          "Archive old blackboard entries. Moves entries older than a cutoff timestamp to an archive file, preserving decision entries. Optionally posts a summary finding.",
        inputSchema: {
          before: z
            .string()
            .refine((val) => !isNaN(Date.parse(val)), {
              message: "Must be a valid ISO 8601 timestamp",
            })
            .optional()
            .describe("ISO timestamp cutoff — archive entries before this time (default: now)"),
          keep_decisions: z
            .boolean()
            .optional()
            .describe("Whether to keep decision entries in the blackboard (default: true)"),
          summarize: z
            .boolean()
            .optional()
            .describe("Whether to post a summary finding after archiving (default: true)"),
        },
      },
      async (args) => {
        try {
          const result = await archiver.archive(args);
          return toolResult(result);
        } catch (e) {
          if (e instanceof TwiningError) {
            return toolError(e.message, e.code);
          }
          return toolError(
            e instanceof Error ? e.message : "Unknown error",
            "INTERNAL_ERROR",
          );
        }
      },
    );
  • Tool handler for 'twining_archive' — delegates to archiver.archive() and returns the result (archived_count, archive_file, summary).
      async (args) => {
        try {
          const result = await archiver.archive(args);
          return toolResult(result);
        } catch (e) {
          if (e instanceof TwiningError) {
            return toolError(e.message, e.code);
          }
          return toolError(
            e instanceof Error ? e.message : "Unknown error",
            "INTERNAL_ERROR",
          );
        }
      },
    );
  • The Archiver class that implements the core archiving logic: reads blackboard entries, partitions by cutoff timestamp (keeping decisions if configured), writes archived entries to archive/{YYYY-MM-DD}-blackboard.jsonl, rewrites blackboard with kept entries, removes embeddings, and optionally posts a summary finding.
    export class Archiver {
      private readonly twiningDir: string;
      private readonly blackboardStore: BlackboardStore;
      private readonly blackboardEngine: BlackboardEngine;
      private readonly indexManager: IndexManager | null;
    
      constructor(
        twiningDir: string,
        blackboardStore: BlackboardStore,
        blackboardEngine: BlackboardEngine,
        indexManager: IndexManager | null,
      ) {
        this.twiningDir = twiningDir;
        this.blackboardStore = blackboardStore;
        this.blackboardEngine = blackboardEngine;
        this.indexManager = indexManager;
      }
    
      /**
       * Archive old blackboard entries.
       * Decision entries are never archived (LIFE-03).
       * Archived entries are moved to archive/{YYYY-MM-DD}-blackboard.jsonl.
       * Optionally posts a summary finding (LIFE-02).
       */
      async archive(options?: {
        before?: string;
        keep_decisions?: boolean;
        summarize?: boolean;
      }): Promise<{
        archived_count: number;
        archive_file: string;
        summary?: string;
      }> {
        const cutoff = options?.before ?? new Date().toISOString();
        const keepDecisions = options?.keep_decisions ?? true;
        const summarize = options?.summarize ?? true;
    
        const bbPath = path.join(this.twiningDir, "blackboard.jsonl");
    
        // Ensure blackboard file exists for locking
        if (!fs.existsSync(bbPath)) {
          return { archived_count: 0, archive_file: "" };
        }
    
        // Lock blackboard for the full read-partition-rewrite cycle
        if (!fs.existsSync(bbPath)) {
          fs.writeFileSync(bbPath, "");
        }
        const release = await lockfile.lock(bbPath, LOCK_OPTIONS);
    
        let toArchive: BlackboardEntry[] = [];
        let toKeep: BlackboardEntry[] = [];
        let archiveFile = "";
    
        try {
          // Read all entries (no lock needed since we hold it)
          const content = fs.readFileSync(bbPath, "utf-8");
          const lines = content
            .split("\n")
            .filter((line) => line.trim().length > 0);
          const allEntries: BlackboardEntry[] = [];
          for (const line of lines) {
            try {
              allEntries.push(JSON.parse(line) as BlackboardEntry);
            } catch {
              // Skip corrupt lines
            }
          }
    
          // Partition: entries before cutoff go to archive, UNLESS they are decisions
          for (const entry of allEntries) {
            const isOldEnough = entry.timestamp < cutoff;
            const isDecision = entry.entry_type === "decision";
            const shouldKeep = !isOldEnough || (keepDecisions && isDecision);
    
            if (shouldKeep) {
              toKeep.push(entry);
            } else {
              toArchive.push(entry);
            }
          }
    
          // If nothing to archive, release lock and return early
          if (toArchive.length === 0) {
            return { archived_count: 0, archive_file: "" };
          }
    
          // Write archived entries to archive file BEFORE rewriting blackboard.
          // This ensures entries exist in the archive before being removed from
          // the blackboard, preventing data loss on crash.
          const archiveDir = path.join(this.twiningDir, "archive");
          ensureDir(archiveDir);
    
          const dateStr = cutoff.slice(0, 10); // YYYY-MM-DD
          archiveFile = path.join(archiveDir, `${dateStr}-blackboard.jsonl`);
    
          // Direct append (no nested lock since we already hold the blackboard lock)
          const archiveContent = toArchive
            .map((e) => JSON.stringify(e))
            .join("\n") + "\n";
          fs.appendFileSync(archiveFile, archiveContent);
    
          // Now rewrite blackboard with only kept entries
          const keptContent =
            toKeep.length > 0
              ? toKeep.map((e) => JSON.stringify(e)).join("\n") + "\n"
              : "";
          fs.writeFileSync(bbPath, keptContent);
        } finally {
          await release();
        }
    
        // Remove archived entry embeddings (best-effort)
        if (this.indexManager) {
          try {
            const archivedIds = toArchive.map((e) => e.id);
            await this.indexManager.removeEntries("blackboard", archivedIds);
          } catch {
            // Best-effort — don't fail archive if embedding cleanup fails
          }
        }
    
        // Build and post summary if requested
        let summaryText: string | undefined;
        if (summarize) {
          summaryText = this.buildSummary(toArchive);
          await this.blackboardEngine.post({
            entry_type: "finding",
            summary: `Archive: ${toArchive.length} entries archived`,
            detail: summaryText,
            tags: ["archive"],
            scope: "project",
          });
        }
    
        return {
          archived_count: toArchive.length,
          archive_file: archiveFile,
          summary: summaryText,
        };
      }
    
      /** Build a human-readable summary of archived entries. */
      private buildSummary(archived: BlackboardEntry[]): string {
        // Group by entry_type
        const groups = new Map<string, BlackboardEntry[]>();
        for (const entry of archived) {
          if (!groups.has(entry.entry_type)) groups.set(entry.entry_type, []);
          groups.get(entry.entry_type)!.push(entry);
        }
    
        const parts: string[] = [];
        parts.push(`Archive summary: ${archived.length} entries archived.`);
    
        for (const [type, entries] of groups) {
          // Sort by timestamp descending for recency
          const sorted = entries.sort((a, b) =>
            b.timestamp.localeCompare(a.timestamp),
          );
          const topSummaries = sorted
            .slice(0, 3)
            .map((e) => e.summary)
            .join("; ");
          parts.push(`${type}: ${entries.length} entries (${topSummaries}).`);
        }
    
        let summary = parts.join(" ");
        // Cap at 2000 chars
        if (summary.length > 2000) {
          summary = summary.slice(0, 1997) + "...";
        }
        return summary;
      }
    }
  • Input schema using Zod: 'before' (ISO 8601 timestamp, optional), 'keep_decisions' (boolean, default true), 'summarize' (boolean, default true).
    inputSchema: {
      before: z
        .string()
        .refine((val) => !isNaN(Date.parse(val)), {
          message: "Must be a valid ISO 8601 timestamp",
        })
        .optional()
        .describe("ISO timestamp cutoff — archive entries before this time (default: now)"),
      keep_decisions: z
        .boolean()
        .optional()
        .describe("Whether to keep decision entries in the blackboard (default: true)"),
      summarize: z
        .boolean()
        .optional()
        .describe("Whether to post a summary finding after archiving (default: true)"),
    },
  • src/server.ts:221-233 (registration)
    Registration call site — 'twining_archive' (via registerLifecycleTools) is only registered when toolMode is 'full'.
    if (toolMode === "full") {
      registerLifecycleTools(
        server,
        twiningDir,
        blackboardStore,
        decisionStore,
        graphStore,
        archiver,
        config,
        agentStore,
      );
      registerGraphTools(server, graphEngine);
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, so the description carries the burden. It discloses that entries are moved to an archive file and that decision entries are preserved, but does not state whether the operation is destructive, if it requires specific permissions, or what the archive file entails.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, front-loaded with the primary action. No unnecessary words. Efficient and clear.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple tool with 3 optional parameters and no output schema, the description covers the main behavior: archiving old entries, preserving decisions, and optional summary. It implies removal from the active blackboard via 'moves to an archive file', which is sufficient. Could mention the archive file location or implications, but not critical.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3. The description adds minimal meaning beyond the schema: it relates 'preserving decision entries' to the keep_decisions parameter and 'optionally posts a summary' to summarize. The before parameter is referenced as 'older than a cutoff timestamp', which is already in the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description clearly states it archives old blackboard entries, moves them to an archive file, and preserves decision entries. However, it does not differentiate from the sibling tool 'twining_archive_stale', which likely performs a similar function.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No explicit guidance on when to use this tool versus alternatives like 'twining_archive_stale'. The description implies usage for archiving old entries but lacks context or exclusion criteria.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/daveangulo/twining-mcp'

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