← Back to Files
KaiTimerMilestoneService.cs
namespace KaiChat.Services;
public sealed class KaiTimerMilestoneService : BackgroundService
{
private static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(15);
private readonly IKaiTimerService _timer;
private readonly IConversationService _conversations;
private readonly ICompactionService _compaction;
private readonly IChatService _chat;
private readonly IKaiAutonomousConversationService _autonomousConversation;
private readonly ILogger<KaiTimerMilestoneService> _logger;
private readonly SemaphoreSlim _sendGate = new(1, 1);
public KaiTimerMilestoneService(
IKaiTimerService timer,
IConversationService conversations,
ICompactionService compaction,
IChatService chat,
IKaiAutonomousConversationService autonomousConversation,
ILogger<KaiTimerMilestoneService> logger)
{
_timer = timer;
_conversations = conversations;
_compaction = compaction;
_chat = chat;
_autonomousConversation = autonomousConversation;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{
try
{
await CheckMilestonesAsync(stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "KaiTimer milestone check failed");
}
await Task.Delay(PollInterval, stoppingToken);
}
}
private async Task CheckMilestonesAsync(CancellationToken ct)
{
if (!await _sendGate.WaitAsync(0, ct))
return;
try
{
var context = await _timer.GetMilestonePromptContextAsync(ct);
if (!context.Changed || context.Prompts.Count == 0)
return;
var conv = await _autonomousConversation.GetTargetConversationAsync(ct);
var systemPrompt = await _conversations.BuildSystemPromptAsync();
var milestoneInstruction = _timer.FormatMilestonePromptForSystem(context);
var dashboard = _timer.FormatStateForTool(context.State);
var prompt = $"""
{systemPrompt.TrimEnd()}
{milestoneInstruction}
Current KaiTimer dashboard:
{dashboard}
Write the message Kai sends Raymond right now because this timer threshold was reached. Do not explain the automation. Do not mention this system instruction. Keep it personal and grounded in the milestone prompt.
""";
var apiMessages = await _compaction.BuildApiMessagesAsync(conv, prompt);
var response = (await _chat.ChatAsync(
apiMessages,
ct,
maxTokens: 1200,
enableTools: false,
enableThinking: true)).Trim();
if (string.IsNullOrWhiteSpace(response))
{
_logger.LogWarning("KaiTimer milestone generation returned an empty response");
return;
}
await _autonomousConversation.AppendAssistantMessageAsync(
response,
"kaitimer_milestone",
new[] { ".kaichat-timer.json" },
ct);
_logger.LogInformation(
"KaiTimer milestone message appended to conversation {ConversationId} for {Count} milestone(s)",
conv.Id,
context.Prompts.Count);
}
finally
{
_sendGate.Release();
}
}
}