Files
gh-jezweb-claude-skills-ski…/references/thread-lifecycle.md
2025-11-30 08:25:15 +08:00

5.8 KiB

Thread Lifecycle Management

Complete guide to managing threads effectively to avoid errors and optimize performance.


Thread States

A thread progresses through these states based on run activity:

Idle (no active runs)
  ↓
Active (run in progress)
  ↓
Requires Action (function calling)
  ↓
Completed / Failed / Cancelled
  ↓
Idle (ready for next run)

Common Patterns

Pattern 1: One Thread Per User

Use Case: Chatbots, support assistants

// In-memory cache (use database in production)
const userThreads = new Map<string, string>();

async function getOrCreateUserThread(userId: string): Promise<string> {
  let threadId = userThreads.get(userId);

  if (!threadId) {
    const thread = await openai.beta.threads.create({
      metadata: { user_id: userId },
    });
    threadId = thread.id;
    userThreads.set(userId, threadId);
  }

  return threadId;
}

Benefits:

  • Conversation continuity
  • Automatic history management
  • Simple architecture

Drawbacks:

  • Long threads consume memory
  • 100k message limit eventually

Pattern 2: Session-Based Threads

Use Case: Temporary conversations

async function createSessionThread(userId: string, sessionId: string) {
  return await openai.beta.threads.create({
    metadata: {
      user_id: userId,
      session_id: sessionId,
      expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
    },
  });
}

Benefits:

  • Clear boundaries
  • Easy cleanup
  • Fresh context

Pattern 3: Topic-Based Threads

Use Case: Multi-topic conversations

async function getTopicThread(userId: string, topic: string) {
  const key = `${userId}:${topic}`;
  // Separate threads for different topics
  return await getOrCreateThread(key);
}

Active Run Management

Check for Active Runs

async function hasActiveRun(threadId: string): Promise<boolean> {
  const runs = await openai.beta.threads.runs.list(threadId, {
    limit: 1,
    order: 'desc',
  });

  const latestRun = runs.data[0];
  return latestRun && ['queued', 'in_progress', 'cancelling'].includes(latestRun.status);
}

Safe Run Creation

async function createRunSafely(
  threadId: string,
  assistantId: string,
  cancelIfActive = true
) {
  // Check for active runs
  const runs = await openai.beta.threads.runs.list(threadId, {
    limit: 1,
    order: 'desc',
  });

  const latestRun = runs.data[0];
  if (latestRun && ['queued', 'in_progress'].includes(latestRun.status)) {
    if (cancelIfActive) {
      await openai.beta.threads.runs.cancel(threadId, latestRun.id);
      // Wait for cancellation
      while (latestRun.status !== 'cancelled') {
        await new Promise(r => setTimeout(r, 500));
        latestRun = await openai.beta.threads.runs.retrieve(threadId, latestRun.id);
      }
    } else {
      throw new Error('Thread has active run');
    }
  }

  return await openai.beta.threads.runs.create(threadId, {
    assistant_id: assistantId,
  });
}

Cleanup Strategies

Time-Based Cleanup

async function cleanupOldThreads(maxAgeHours: number = 24) {
  // Query your database for old threads
  const oldThreads = await db.getThreadsOlderThan(maxAgeHours);

  for (const threadId of oldThreads) {
    await openai.beta.threads.del(threadId);
    await db.deleteThreadRecord(threadId);
  }
}

Message Count-Based

async function archiveIfTooLong(threadId: string, maxMessages: number = 1000) {
  const messages = await openai.beta.threads.messages.list(threadId);

  if (messages.data.length >= maxMessages) {
    // Archive to database
    await db.archiveThread(threadId, messages.data);

    // Create new thread
    return await openai.beta.threads.create({
      metadata: { previous_thread: threadId },
    });
  }

  return threadId;
}

Safe Deletion

async function safeDeleteThread(threadId: string) {
  // Cancel all active runs first
  const runs = await openai.beta.threads.runs.list(threadId);

  for (const run of runs.data) {
    if (['queued', 'in_progress'].includes(run.status)) {
      await openai.beta.threads.runs.cancel(threadId, run.id);
    }
  }

  // Wait a moment for cancellations
  await new Promise(r => setTimeout(r, 1000));

  // Delete thread
  await openai.beta.threads.del(threadId);
}

Error Handling

Concurrent Run Prevention

class ThreadManager {
  private locks = new Map<string, Promise<any>>();

  async executeRun(threadId: string, assistantId: string) {
    // Wait if another run is in progress
    if (this.locks.has(threadId)) {
      await this.locks.get(threadId);
    }

    // Create lock
    const runPromise = this._runAssistant(threadId, assistantId);
    this.locks.set(threadId, runPromise);

    try {
      return await runPromise;
    } finally {
      this.locks.delete(threadId);
    }
  }

  private async _runAssistant(threadId: string, assistantId: string) {
    // Run logic here
  }
}

Best Practices

  1. Always check for active runs before creating new ones
  2. Use metadata to track ownership and expiration
  3. Implement cleanup to manage costs
  4. Set reasonable limits (message count, age)
  5. Handle errors gracefully (active run conflicts)
  6. Archive old conversations before deletion
  7. Use locks for concurrent access

Monitoring

async function getThreadStats(threadId: string) {
  const thread = await openai.beta.threads.retrieve(threadId);
  const messages = await openai.beta.threads.messages.list(threadId);
  const runs = await openai.beta.threads.runs.list(threadId);

  return {
    threadId: thread.id,
    createdAt: new Date(thread.created_at * 1000),
    messageCount: messages.data.length,
    runCount: runs.data.length,
    lastRun: runs.data[0],
    metadata: thread.metadata,
  };
}

Last Updated: 2025-10-25