Size: 2.6 KB Modified: 25/06/2026 12:37 PM
using System.Collections.Concurrent;
using System.Text.Json;
using KaiChat.Models;

namespace KaiChat.Services;

public class ToolCallLogService : IToolCallLogService
{
    private readonly ConcurrentQueue<ToolCallLog> _entries = new();
    private const int MaxEntries = 500;
    private readonly string _filePath;
    private readonly object _saveLock = new();

    public ToolCallLogService(string filePath)
    {
        _filePath = filePath;
        LoadFromDisk();
    }

    public string LogFilePath => _filePath;

    public void Add(ToolCallLog entry)
    {
        _entries.Enqueue(entry);
        while (_entries.Count > MaxEntries)
            _entries.TryDequeue(out _);
        SaveToDisk();
    }

    public List<ToolCallLog> GetRecent(int count = 100)
    {
        return _entries.OrderByDescending(l => l.Timestamp).Take(count).ToList();
    }

    public void Clear()
    {
        while (_entries.TryDequeue(out _)) { }
        SaveToDisk();
    }

    public int GetTotalCount()
    {
        return _entries.Count;
    }

    private void LoadFromDisk()
    {
        try
        {
            if (!File.Exists(_filePath)) return;
            var json = File.ReadAllText(_filePath);
            var logs = JsonSerializer.Deserialize<List<ToolCallLog>>(json);
            if (logs == null) return;
            // File may be in any order (old format was newest-first). Enqueue oldest-first
            // so the queue order is consistent and GetRecent's Reverse() gives newest-first.
            logs = logs.OrderBy(l => l.Timestamp).ToList();
            foreach (var log in logs)
            {
                _entries.Enqueue(log);
                if (_entries.Count > MaxEntries)
                    _entries.TryDequeue(out _);
            }
        }
        catch
        {
            // Corrupted file — ignore, start fresh
        }
    }

    private void SaveToDisk()
    {
        try
        {
            var dir = Path.GetDirectoryName(_filePath);
            if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
                Directory.CreateDirectory(dir);

            // Store oldest-first — matches the natural queue order
            var logs = _entries.Take(MaxEntries).ToList();
            var json = JsonSerializer.Serialize(logs, new JsonSerializerOptions { WriteIndented = true });
            lock (_saveLock)
            {
                File.WriteAllText(_filePath, json);
            }
        }
        catch
        {
            // Non-critical persistence failure — don't crash
        }
    }
}
Offline