- claude-code compact 흐름을 참고해 AX에 session memory compact, microcompact, collapse/snip 단계를 추가하고 ContextCondenser를 단계별 결과 반환 구조로 확장함 - ChatConversation과 AX Agent 하단 컨텍스트 카드에 대화별 compact 누적 회수, 절감 토큰, session memory/microcompact/snip 집계를 반영함 - AGENTS.md에 개발 단계부터 최적화와 실행 속도를 우선 고려하는 작업 지침을 추가함 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
This commit is contained in:
@@ -203,6 +203,12 @@ if (!enabled) return ToolResult.Ok("비활성 상태입니다. 설정에서 활
|
|||||||
- 모든 변경 후 `dotnet build` 실행 → **경고 0, 오류 0** 필수
|
- 모든 변경 후 `dotnet build` 실행 → **경고 0, 오류 0** 필수
|
||||||
- CS8603 (nullable) 경고 즉시 수정
|
- CS8603 (nullable) 경고 즉시 수정
|
||||||
|
|
||||||
|
### 성능/실행속도 우선 원칙
|
||||||
|
- 기능 구현 시 가능하면 **개발 단계부터 최적화와 실행 속도**를 함께 고려합니다.
|
||||||
|
- 동일 품질을 만족하는 구현안이 여러 개라면, **더 가볍고 빠르게 동작하는 구조**를 우선 채택합니다.
|
||||||
|
- UI/UX 개선, 에이전트 루프, 도구 실행, 컨텍스트 압축, 검색/필터링 기능은 특히 초기 구현부터 불필요한 반복 계산·과도한 렌더링·중복 I/O를 줄이는 방향으로 설계합니다.
|
||||||
|
- 단, 성능 최적화를 이유로 가독성이나 안정성을 과도하게 해치지 않으며, **동등 품질 + 유지보수 가능성**을 함께 만족하는 수준에서 최적화합니다.
|
||||||
|
|
||||||
### 리소스 관리
|
### 리소스 관리
|
||||||
- `IDisposable` 구현 객체는 반드시 해제 (PerformanceCounter, LspClientService 등)
|
- `IDisposable` 구현 객체는 반드시 해제 (PerformanceCounter, LspClientService 등)
|
||||||
- P/Invoke 메모리: `Marshal.AllocHGlobal` 후 `finally`에서 `FreeHGlobal`
|
- P/Invoke 메모리: `Marshal.AllocHGlobal` 후 `finally`에서 `FreeHGlobal`
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
|
|||||||
`docs/claw-code-parity-plan.md`
|
`docs/claw-code-parity-plan.md`
|
||||||
|
|
||||||
|
|
||||||
|
- 업데이트: 2026-04-04 23:47 (KST)
|
||||||
|
- AX Agent 컨텍스트 압축 경로에 `session memory compact`, `microcompact`, `collapse/snip` 단계를 추가해 오래된 요약·실행 로그·도구 결과를 LLM 요약 전에 더 세밀하게 줄이도록 보강했습니다.
|
||||||
|
- 현재 대화 기준 compact 누적 회수, 자동/수동 비중, 절감 토큰, session memory 적용 횟수, microcompact/snipped 메시지 수를 하단 컨텍스트 카드 hover에서 함께 확인할 수 있게 했습니다.
|
||||||
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0
|
||||||
|
|
||||||
|
|
||||||
- 업데이트: 2026-04-04 23:32 (KST)
|
- 업데이트: 2026-04-04 23:32 (KST)
|
||||||
- AX Agent 컨텍스트 압축 경로에 `microcompact` 성격의 선행 경량 압축 단계를 추가해, 오래된 실행 로그·도구 결과·긴 메시지를 먼저 경계 요약으로 줄인 뒤 LLM 요약 단계로 넘기도록 보강했습니다.
|
- AX Agent 컨텍스트 압축 경로에 `microcompact` 성격의 선행 경량 압축 단계를 추가해, 오래된 실행 로그·도구 결과·긴 메시지를 먼저 경계 요약으로 줄인 뒤 LLM 요약 단계로 넘기도록 보강했습니다.
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# AX Copilot - ?? ??
|
# AX Copilot - 개발 문서
|
||||||
|
|
||||||
|
- Document update: 2026-04-04 23:47 (KST) - Added AX-side session-memory compaction, collapse/snip trimming, and per-conversation compaction usage tracking so compact now records staged reductions instead of only the last before/after snapshot.
|
||||||
|
- Document update: 2026-04-04 23:47 (KST) - The AX Agent context tooltip now shows session totals for compaction count, saved tokens, session-memory passes, microcompact boundaries, and snipped messages for the current conversation.
|
||||||
|
|
||||||
- Document update: 2026-04-04 23:32 (KST) - Upgraded ContextCondenser from a two-step path to a three-step path (tool-result truncate -> microcompact -> summarize) so long sessions can strip older execution/tool chatter before invoking the LLM summary stage.
|
- Document update: 2026-04-04 23:32 (KST) - Upgraded ContextCondenser from a two-step path to a three-step path (tool-result truncate -> microcompact -> summarize) so long sessions can strip older execution/tool chatter before invoking the LLM summary stage.
|
||||||
- Document update: 2026-04-04 23:32 (KST) - Added AX-side microcompact boundaries that compress older tool results, execution metadata, and oversized messages into lighter boundary summaries, bringing the compact flow closer to the staged approach used in claude-code.
|
- Document update: 2026-04-04 23:32 (KST) - Added AX-side microcompact boundaries that compress older tool results, execution metadata, and oversized messages into lighter boundary summaries, bringing the compact flow closer to the staged approach used in claude-code.
|
||||||
|
|||||||
@@ -139,6 +139,42 @@ public class ChatConversation
|
|||||||
|
|
||||||
[JsonPropertyName("conversationSortMode")]
|
[JsonPropertyName("conversationSortMode")]
|
||||||
public string ConversationSortMode { get; set; } = "activity";
|
public string ConversationSortMode { get; set; } = "activity";
|
||||||
|
|
||||||
|
[JsonPropertyName("compactionCount")]
|
||||||
|
public int CompactionCount { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("automaticCompactionCount")]
|
||||||
|
public int AutomaticCompactionCount { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("manualCompactionCount")]
|
||||||
|
public int ManualCompactionCount { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("compactionSavedTokens")]
|
||||||
|
public int CompactionSavedTokens { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("sessionMemoryCompactionCount")]
|
||||||
|
public int SessionMemoryCompactionCount { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("microcompactBoundaryCount")]
|
||||||
|
public int MicrocompactBoundaryCount { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("snipCompactionCount")]
|
||||||
|
public int SnipCompactionCount { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("lastCompactionAt")]
|
||||||
|
public DateTime? LastCompactionAt { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("lastCompactionWasAutomatic")]
|
||||||
|
public bool LastCompactionWasAutomatic { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("lastCompactionBeforeTokens")]
|
||||||
|
public int? LastCompactionBeforeTokens { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("lastCompactionAfterTokens")]
|
||||||
|
public int? LastCompactionAfterTokens { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("lastCompactionStageSummary")]
|
||||||
|
public string? LastCompactionStageSummary { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ChatAgentRunRecord
|
public class ChatAgentRunRecord
|
||||||
|
|||||||
@@ -2,14 +2,32 @@
|
|||||||
|
|
||||||
namespace AxCopilot.Services.Agent;
|
namespace AxCopilot.Services.Agent;
|
||||||
|
|
||||||
|
public sealed class ContextCompactionResult
|
||||||
|
{
|
||||||
|
public bool Changed { get; set; }
|
||||||
|
public int BeforeTokens { get; set; }
|
||||||
|
public int AfterTokens { get; set; }
|
||||||
|
public bool SessionMemoryApplied { get; set; }
|
||||||
|
public int SessionMemoryMergedMessages { get; set; }
|
||||||
|
public int MicrocompactBoundaryCount { get; set; }
|
||||||
|
public int MicrocompactSingleCount { get; set; }
|
||||||
|
public int SnippedMessageCount { get; set; }
|
||||||
|
public int CollapsedBoundaryCount { get; set; }
|
||||||
|
public List<string> AppliedStages { get; } = new();
|
||||||
|
public int SavedTokens => Math.Max(0, BeforeTokens - AfterTokens);
|
||||||
|
public string StageSummary => AppliedStages.Count == 0 ? "없음" : string.Join(" -> ", AppliedStages);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 컨텍스트 윈도우 관리: 대화가 길어지면 이전 메시지를 요약하여 압축합니다.
|
/// 컨텍스트 윈도우 관리: 대화가 길어지면 이전 메시지를 요약하여 압축합니다.
|
||||||
/// MemGPT/OpenHands의 LLMSummarizingCondenser 패턴을 경량 구현.
|
/// MemGPT/OpenHands의 LLMSummarizingCondenser 패턴을 경량 구현.
|
||||||
///
|
///
|
||||||
/// 3단계 압축 전략:
|
/// 5단계 압축 전략:
|
||||||
/// 1단계 — 도구 결과 자르기: 대용량 tool_result 출력을 핵심만 남기고 축약 (LLM 호출 없음)
|
/// 1단계 — 도구 결과 자르기: 대용량 tool_result 출력을 핵심만 남기고 축약 (LLM 호출 없음)
|
||||||
/// 2단계 — microcompact: 오래된 실행 로그/도구 묶음을 경계 요약으로 치환 (LLM 호출 없음)
|
/// 2단계 — session memory compact: 이전 경계/요약을 하나의 세션 메모로 통합
|
||||||
/// 3단계 — 이전 대화 요약: 오래된 메시지를 LLM으로 요약하여 교체 (LLM 1회 호출)
|
/// 3단계 — microcompact: 오래된 실행 로그/도구 묶음을 경계 요약으로 치환 (LLM 호출 없음)
|
||||||
|
/// 4단계 — collapse/snip: 긴 로그/도구 결과를 더 짧은 경계/앞뒤 일부로 절단
|
||||||
|
/// 5단계 — 이전 대화 요약: 오래된 메시지를 LLM으로 요약하여 교체 (LLM 1회 호출)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ContextCondenser
|
public static class ContextCondenser
|
||||||
{
|
{
|
||||||
@@ -17,6 +35,8 @@ public static class ContextCondenser
|
|||||||
private const int MaxToolResultChars = 1500;
|
private const int MaxToolResultChars = 1500;
|
||||||
private const int MicrocompactSingleKeepChars = 480;
|
private const int MicrocompactSingleKeepChars = 480;
|
||||||
private const int MicrocompactGroupMinCount = 2;
|
private const int MicrocompactGroupMinCount = 2;
|
||||||
|
private const int SnipKeepHeadChars = 220;
|
||||||
|
private const int SnipKeepTailChars = 140;
|
||||||
|
|
||||||
/// <summary>요약 시 유지할 최근 메시지 수</summary>
|
/// <summary>요약 시 유지할 최근 메시지 수</summary>
|
||||||
private const int RecentKeepCount = 6;
|
private const int RecentKeepCount = 6;
|
||||||
@@ -51,8 +71,22 @@ public static class ContextCondenser
|
|||||||
bool force = false,
|
bool force = false,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (messages.Count < 6) return false;
|
var result = await CondenseWithStatsAsync(messages, llm, maxOutputTokens, proactiveEnabled, triggerPercent, force, ct);
|
||||||
if (!force && !proactiveEnabled) return false;
|
return result.Changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<ContextCompactionResult> CondenseWithStatsAsync(
|
||||||
|
List<ChatMessage> messages,
|
||||||
|
LlmService llm,
|
||||||
|
int maxOutputTokens,
|
||||||
|
bool proactiveEnabled = true,
|
||||||
|
int triggerPercent = 80,
|
||||||
|
bool force = false,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var result = new ContextCompactionResult();
|
||||||
|
if (messages.Count < 6) return result;
|
||||||
|
if (!force && !proactiveEnabled) return result;
|
||||||
|
|
||||||
// 현재 모델의 입력 토큰 한도
|
// 현재 모델의 입력 토큰 한도
|
||||||
var settings = llm.GetCurrentModelInfo();
|
var settings = llm.GetCurrentModelInfo();
|
||||||
@@ -62,34 +96,106 @@ public static class ContextCondenser
|
|||||||
var threshold = (int)(effectiveMax * (percent / 100.0)); // 설정 임계치에서 압축 시작
|
var threshold = (int)(effectiveMax * (percent / 100.0)); // 설정 임계치에서 압축 시작
|
||||||
|
|
||||||
var currentTokens = TokenEstimator.EstimateMessages(messages);
|
var currentTokens = TokenEstimator.EstimateMessages(messages);
|
||||||
if (!force && currentTokens < threshold) return false;
|
result.BeforeTokens = currentTokens;
|
||||||
|
if (!force && currentTokens < threshold)
|
||||||
|
{
|
||||||
|
result.AfterTokens = currentTokens;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
var originalTokens = currentTokens;
|
var originalTokens = currentTokens;
|
||||||
|
|
||||||
bool didCompress = false;
|
bool didCompress = false;
|
||||||
|
|
||||||
// ── 1단계: 도구 결과 축약 (LLM 호출 없음, 즉시 실행) ──
|
// ── 1단계: 도구 결과 축약 (LLM 호출 없음, 즉시 실행) ──
|
||||||
didCompress |= TruncateToolResults(messages);
|
if (TruncateToolResults(messages))
|
||||||
|
{
|
||||||
|
didCompress = true;
|
||||||
|
result.AppliedStages.Add("tool-result");
|
||||||
|
}
|
||||||
|
|
||||||
// 1단계 후 다시 추정
|
// 1단계 후 다시 추정
|
||||||
currentTokens = TokenEstimator.EstimateMessages(messages);
|
currentTokens = TokenEstimator.EstimateMessages(messages);
|
||||||
if (!force && currentTokens < threshold) return didCompress;
|
if (!force && currentTokens < threshold)
|
||||||
|
{
|
||||||
|
result.Changed = didCompress;
|
||||||
|
result.AfterTokens = currentTokens;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 2단계: 오래된 실행/도구 묶음 경량 압축 ──
|
// ── 2단계: 이전 경계/요약을 세션 메모로 통합 ──
|
||||||
didCompress |= MicrocompactOlderMessages(messages);
|
var sessionMemoryMergedMessages = SessionMemoryCompactOlderMessages(messages);
|
||||||
|
if (sessionMemoryMergedMessages > 0)
|
||||||
|
{
|
||||||
|
didCompress = true;
|
||||||
|
result.SessionMemoryApplied = true;
|
||||||
|
result.SessionMemoryMergedMessages = sessionMemoryMergedMessages;
|
||||||
|
result.AppliedStages.Add("session-memory");
|
||||||
|
}
|
||||||
|
|
||||||
currentTokens = TokenEstimator.EstimateMessages(messages);
|
currentTokens = TokenEstimator.EstimateMessages(messages);
|
||||||
if (!force && currentTokens < threshold) return didCompress;
|
if (!force && currentTokens < threshold)
|
||||||
|
{
|
||||||
|
result.Changed = didCompress;
|
||||||
|
result.AfterTokens = currentTokens;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 3단계: 이전 대화 LLM 요약 ──
|
// ── 3단계: 오래된 실행/도구 묶음 경량 압축 ──
|
||||||
didCompress |= await SummarizeOldMessagesAsync(messages, llm, ct);
|
var (boundaryCount, singleCount) = MicrocompactOlderMessages(messages);
|
||||||
|
if (boundaryCount > 0 || singleCount > 0)
|
||||||
|
{
|
||||||
|
didCompress = true;
|
||||||
|
result.MicrocompactBoundaryCount = boundaryCount;
|
||||||
|
result.MicrocompactSingleCount = singleCount;
|
||||||
|
result.AppliedStages.Add("microcompact");
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTokens = TokenEstimator.EstimateMessages(messages);
|
||||||
|
if (!force && currentTokens < threshold)
|
||||||
|
{
|
||||||
|
result.Changed = didCompress;
|
||||||
|
result.AfterTokens = currentTokens;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4단계: collapse/snip ──
|
||||||
|
var (snippedCount, collapsedCount) = CollapseAndSnipOlderMessages(messages);
|
||||||
|
if (snippedCount > 0 || collapsedCount > 0)
|
||||||
|
{
|
||||||
|
didCompress = true;
|
||||||
|
result.SnippedMessageCount = snippedCount;
|
||||||
|
result.CollapsedBoundaryCount = collapsedCount;
|
||||||
|
result.AppliedStages.Add("snip");
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTokens = TokenEstimator.EstimateMessages(messages);
|
||||||
|
if (!force && currentTokens < threshold)
|
||||||
|
{
|
||||||
|
result.Changed = didCompress;
|
||||||
|
result.AfterTokens = currentTokens;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 5단계: 이전 대화 LLM 요약 ──
|
||||||
|
if (await SummarizeOldMessagesAsync(messages, llm, ct))
|
||||||
|
{
|
||||||
|
didCompress = true;
|
||||||
|
result.AppliedStages.Add("summary");
|
||||||
|
}
|
||||||
|
|
||||||
if (didCompress)
|
if (didCompress)
|
||||||
{
|
{
|
||||||
var afterTokens = TokenEstimator.EstimateMessages(messages);
|
var afterTokens = TokenEstimator.EstimateMessages(messages);
|
||||||
LogService.Info($"Context Condenser: {originalTokens} → {afterTokens} 토큰 (절감 {originalTokens - afterTokens})");
|
LogService.Info($"Context Condenser: {originalTokens} → {afterTokens} 토큰 (절감 {originalTokens - afterTokens})");
|
||||||
|
result.Changed = true;
|
||||||
|
result.AfterTokens = afterTokens;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.AfterTokens = currentTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
return didCompress;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -164,11 +270,11 @@ public static class ContextCondenser
|
|||||||
/// 오래된 실행 로그/도구 결과/비정상적으로 긴 메시지를 먼저 경량 경계 요약으로 묶습니다.
|
/// 오래된 실행 로그/도구 결과/비정상적으로 긴 메시지를 먼저 경량 경계 요약으로 묶습니다.
|
||||||
/// claw-code의 microcompact처럼 LLM 호출 전에 토큰을 한 번 더 줄이는 단계입니다.
|
/// claw-code의 microcompact처럼 LLM 호출 전에 토큰을 한 번 더 줄이는 단계입니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool MicrocompactOlderMessages(List<ChatMessage> messages)
|
private static (int BoundaryCount, int SingleCount) MicrocompactOlderMessages(List<ChatMessage> messages)
|
||||||
{
|
{
|
||||||
var systemMsg = messages.FirstOrDefault(m => m.Role == "system");
|
var systemMsg = messages.FirstOrDefault(m => m.Role == "system");
|
||||||
var nonSystemMessages = messages.Where(m => m.Role != "system").ToList();
|
var nonSystemMessages = messages.Where(m => m.Role != "system").ToList();
|
||||||
if (nonSystemMessages.Count <= RecentKeepCount + 2) return false;
|
if (nonSystemMessages.Count <= RecentKeepCount + 2) return (0, 0);
|
||||||
|
|
||||||
var keepCount = Math.Min(RecentKeepCount, nonSystemMessages.Count);
|
var keepCount = Math.Min(RecentKeepCount, nonSystemMessages.Count);
|
||||||
var recentMessages = nonSystemMessages.Skip(nonSystemMessages.Count - keepCount).ToList();
|
var recentMessages = nonSystemMessages.Skip(nonSystemMessages.Count - keepCount).ToList();
|
||||||
@@ -176,6 +282,8 @@ public static class ContextCondenser
|
|||||||
|
|
||||||
var rewritten = new List<ChatMessage>(oldMessages.Count);
|
var rewritten = new List<ChatMessage>(oldMessages.Count);
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
|
int boundaryCount = 0;
|
||||||
|
int singleCount = 0;
|
||||||
|
|
||||||
for (int i = 0; i < oldMessages.Count;)
|
for (int i = 0; i < oldMessages.Count;)
|
||||||
{
|
{
|
||||||
@@ -194,12 +302,14 @@ public static class ContextCondenser
|
|||||||
{
|
{
|
||||||
rewritten.Add(BuildMicrocompactBoundary(group));
|
rewritten.Add(BuildMicrocompactBoundary(group));
|
||||||
changed = true;
|
changed = true;
|
||||||
|
boundaryCount++;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var singleChanged = TryMicrocompactSingle(group[0], out var compacted);
|
var singleChanged = TryMicrocompactSingle(group[0], out var compacted);
|
||||||
rewritten.Add(singleChanged ? compacted : group[0]);
|
rewritten.Add(singleChanged ? compacted : group[0]);
|
||||||
changed |= singleChanged;
|
changed |= singleChanged;
|
||||||
|
if (singleChanged) singleCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
i = j;
|
i = j;
|
||||||
@@ -210,6 +320,7 @@ public static class ContextCondenser
|
|||||||
{
|
{
|
||||||
rewritten.Add(singleCompacted);
|
rewritten.Add(singleCompacted);
|
||||||
changed = true;
|
changed = true;
|
||||||
|
singleCount++;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -219,12 +330,204 @@ public static class ContextCondenser
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!changed) return false;
|
if (!changed) return (0, 0);
|
||||||
|
|
||||||
messages.Clear();
|
messages.Clear();
|
||||||
if (systemMsg != null) messages.Add(systemMsg);
|
if (systemMsg != null) messages.Add(systemMsg);
|
||||||
messages.AddRange(rewritten);
|
messages.AddRange(rewritten);
|
||||||
messages.AddRange(recentMessages);
|
messages.AddRange(recentMessages);
|
||||||
|
return (boundaryCount, singleCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SessionMemoryCompactOlderMessages(List<ChatMessage> messages)
|
||||||
|
{
|
||||||
|
var systemMsg = messages.FirstOrDefault(m => m.Role == "system");
|
||||||
|
var nonSystemMessages = messages.Where(m => m.Role != "system").ToList();
|
||||||
|
if (nonSystemMessages.Count <= RecentKeepCount + 3) return 0;
|
||||||
|
|
||||||
|
var keepCount = Math.Min(RecentKeepCount, nonSystemMessages.Count);
|
||||||
|
var recentMessages = nonSystemMessages.Skip(nonSystemMessages.Count - keepCount).ToList();
|
||||||
|
var oldMessages = nonSystemMessages.Take(nonSystemMessages.Count - keepCount).ToList();
|
||||||
|
|
||||||
|
var candidates = oldMessages
|
||||||
|
.Select((message, index) => new { message, index })
|
||||||
|
.Where(x => IsSessionMemoryCandidate(x.message))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (candidates.Count < 2) return 0;
|
||||||
|
|
||||||
|
var attachedFiles = candidates
|
||||||
|
.SelectMany(x => x.message.AttachedFiles ?? Enumerable.Empty<string>())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(path =>
|
||||||
|
{
|
||||||
|
try { return System.IO.Path.GetFileName(path); }
|
||||||
|
catch { return path; }
|
||||||
|
})
|
||||||
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
|
.Take(5)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var merged = new ChatMessage
|
||||||
|
{
|
||||||
|
Role = "assistant",
|
||||||
|
Timestamp = candidates.Last().message.Timestamp,
|
||||||
|
MetaKind = "session_memory_compaction",
|
||||||
|
MetaRunId = candidates.Last().message.MetaRunId,
|
||||||
|
AttachedFiles = attachedFiles.Count > 0 ? attachedFiles : null,
|
||||||
|
Content = string.Join("\n", new[]
|
||||||
|
{
|
||||||
|
$"[세션 메모리 압축 - {candidates.Count}개 이전 경계 통합]",
|
||||||
|
"- 이전 요약, 실행 경계, 오래된 메타 로그를 하나의 세션 메모로 합쳤습니다.",
|
||||||
|
attachedFiles.Count > 0 ? $"- 관련 파일: {string.Join(", ", attachedFiles)}" : "- 관련 파일: 없음",
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
var rewritten = new List<ChatMessage>(oldMessages.Count - candidates.Count + 1);
|
||||||
|
bool inserted = false;
|
||||||
|
for (int i = 0; i < oldMessages.Count; i++)
|
||||||
|
{
|
||||||
|
if (candidates.Any(x => x.index == i))
|
||||||
|
{
|
||||||
|
if (!inserted)
|
||||||
|
{
|
||||||
|
rewritten.Add(merged);
|
||||||
|
inserted = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
rewritten.Add(oldMessages[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.Clear();
|
||||||
|
if (systemMsg != null) messages.Add(systemMsg);
|
||||||
|
messages.AddRange(rewritten);
|
||||||
|
messages.AddRange(recentMessages);
|
||||||
|
return candidates.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSessionMemoryCandidate(ChatMessage message)
|
||||||
|
{
|
||||||
|
var content = message.Content ?? "";
|
||||||
|
if (string.IsNullOrWhiteSpace(content)) return false;
|
||||||
|
|
||||||
|
return string.Equals(message.MetaKind, "microcompact_boundary", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(message.MetaKind, "session_memory_compaction", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| content.StartsWith("[이전 대화 요약", StringComparison.Ordinal)
|
||||||
|
|| content.StartsWith("[이전 실행 묶음 압축", StringComparison.Ordinal)
|
||||||
|
|| content.StartsWith("[이전 도구 결과 축약", StringComparison.Ordinal)
|
||||||
|
|| content.StartsWith("[이전 도구 호출 묶음 축약", StringComparison.Ordinal)
|
||||||
|
|| content.StartsWith("[이전 실행 메타 축약", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int SnippedCount, int CollapsedCount) CollapseAndSnipOlderMessages(List<ChatMessage> messages)
|
||||||
|
{
|
||||||
|
var systemMsg = messages.FirstOrDefault(m => m.Role == "system");
|
||||||
|
var nonSystemMessages = messages.Where(m => m.Role != "system").ToList();
|
||||||
|
if (nonSystemMessages.Count <= RecentKeepCount + 2) return (0, 0);
|
||||||
|
|
||||||
|
var keepCount = Math.Min(RecentKeepCount, nonSystemMessages.Count);
|
||||||
|
var recentMessages = nonSystemMessages.Skip(nonSystemMessages.Count - keepCount).ToList();
|
||||||
|
var oldMessages = nonSystemMessages.Take(nonSystemMessages.Count - keepCount).ToList();
|
||||||
|
|
||||||
|
var rewritten = new List<ChatMessage>(oldMessages.Count);
|
||||||
|
int snippedCount = 0;
|
||||||
|
int collapsedCount = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < oldMessages.Count;)
|
||||||
|
{
|
||||||
|
var current = oldMessages[i];
|
||||||
|
if (IsBoundaryLike(current))
|
||||||
|
{
|
||||||
|
var group = new List<ChatMessage> { current };
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < oldMessages.Count && IsBoundaryLike(oldMessages[j]))
|
||||||
|
{
|
||||||
|
group.Add(oldMessages[j]);
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.Count >= 2)
|
||||||
|
{
|
||||||
|
rewritten.Add(new ChatMessage
|
||||||
|
{
|
||||||
|
Role = "assistant",
|
||||||
|
Timestamp = group.Last().Timestamp,
|
||||||
|
MetaKind = "collapsed_boundary",
|
||||||
|
MetaRunId = group.Last().MetaRunId,
|
||||||
|
Content = $"[이전 압축 경계 병합 - {group.Count}개]\n- 이전 compact/snippet 경계를 하나로 합쳤습니다."
|
||||||
|
});
|
||||||
|
collapsedCount += group.Count;
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TrySnipMessage(current, out var snipped))
|
||||||
|
{
|
||||||
|
rewritten.Add(snipped);
|
||||||
|
snippedCount++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rewritten.Add(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snippedCount == 0 && collapsedCount == 0) return (0, 0);
|
||||||
|
|
||||||
|
messages.Clear();
|
||||||
|
if (systemMsg != null) messages.Add(systemMsg);
|
||||||
|
messages.AddRange(rewritten);
|
||||||
|
messages.AddRange(recentMessages);
|
||||||
|
return (snippedCount, collapsedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsBoundaryLike(ChatMessage message)
|
||||||
|
{
|
||||||
|
var content = message.Content ?? "";
|
||||||
|
return string.Equals(message.MetaKind, "microcompact_boundary", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(message.MetaKind, "session_memory_compaction", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| content.StartsWith("[이전 실행 묶음 압축", StringComparison.Ordinal)
|
||||||
|
|| content.StartsWith("[세션 메모리 압축", StringComparison.Ordinal)
|
||||||
|
|| content.StartsWith("[이전 압축 경계 병합", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TrySnipMessage(ChatMessage source, out ChatMessage snipped)
|
||||||
|
{
|
||||||
|
snipped = source;
|
||||||
|
var content = source.Content ?? "";
|
||||||
|
if (string.IsNullOrWhiteSpace(content)) return false;
|
||||||
|
if (source.Role == "system") return false;
|
||||||
|
if (content.Length < 900 && content.Split('\n').Length < 12) return false;
|
||||||
|
|
||||||
|
var normalized = content.Replace("\r\n", "\n");
|
||||||
|
var lines = normalized
|
||||||
|
.Split('\n')
|
||||||
|
.Select(line => line.TrimEnd())
|
||||||
|
.Where(line => !string.IsNullOrWhiteSpace(line))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (lines.Count >= 6)
|
||||||
|
{
|
||||||
|
var headLines = lines.Take(3);
|
||||||
|
var tailLines = lines.TakeLast(2);
|
||||||
|
snipped = CloneWithContent(
|
||||||
|
source,
|
||||||
|
string.Join("\n", headLines) +
|
||||||
|
"\n...[snip: 중간 실행 로그/출력 축약]...\n" +
|
||||||
|
string.Join("\n", tailLines));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var head = normalized[..Math.Min(SnipKeepHeadChars, normalized.Length)];
|
||||||
|
var tail = normalized.Length > SnipKeepTailChars ? normalized[^SnipKeepTailChars..] : "";
|
||||||
|
snipped = CloneWithContent(
|
||||||
|
source,
|
||||||
|
head + "\n...[snip: 중간 내용 축약]...\n" + tail);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,14 @@ public partial class ChatWindow : Window
|
|||||||
private int? _lastCompactionAfterTokens;
|
private int? _lastCompactionAfterTokens;
|
||||||
private DateTime? _lastCompactionAt;
|
private DateTime? _lastCompactionAt;
|
||||||
private bool _lastCompactionWasAutomatic;
|
private bool _lastCompactionWasAutomatic;
|
||||||
|
private string _lastCompactionStageSummary = "";
|
||||||
|
private int _sessionCompactionCount;
|
||||||
|
private int _sessionAutomaticCompactionCount;
|
||||||
|
private int _sessionManualCompactionCount;
|
||||||
|
private int _sessionCompactionSavedTokens;
|
||||||
|
private int _sessionMemoryCompactionCount;
|
||||||
|
private int _sessionMicrocompactBoundaryCount;
|
||||||
|
private int _sessionSnipCompactionCount;
|
||||||
private void ApplyQuickActionVisual(Button button, bool active, string activeBg, string activeFg)
|
private void ApplyQuickActionVisual(Button button, bool active, string activeBg, string activeFg)
|
||||||
{
|
{
|
||||||
if (button?.Content is not string text)
|
if (button?.Content is not string text)
|
||||||
@@ -1686,6 +1694,7 @@ public partial class ChatWindow : Window
|
|||||||
}
|
}
|
||||||
// 대화별 설정 복원 (없으면 전역 기본값)
|
// 대화별 설정 복원 (없으면 전역 기본값)
|
||||||
LoadConversationSettings();
|
LoadConversationSettings();
|
||||||
|
LoadCompactionMetricsFromConversation();
|
||||||
UpdatePermissionUI();
|
UpdatePermissionUI();
|
||||||
UpdateDataUsageUI();
|
UpdateDataUsageUI();
|
||||||
RefreshContextUsageVisual();
|
RefreshContextUsageVisual();
|
||||||
@@ -1711,6 +1720,25 @@ public partial class ChatWindow : Window
|
|||||||
_selectedMood = conv?.Mood ?? llm.DefaultMood ?? "modern";
|
_selectedMood = conv?.Mood ?? llm.DefaultMood ?? "modern";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LoadCompactionMetricsFromConversation()
|
||||||
|
{
|
||||||
|
ChatConversation? conv;
|
||||||
|
lock (_convLock) conv = _currentConversation;
|
||||||
|
|
||||||
|
_sessionCompactionCount = conv?.CompactionCount ?? 0;
|
||||||
|
_sessionAutomaticCompactionCount = conv?.AutomaticCompactionCount ?? 0;
|
||||||
|
_sessionManualCompactionCount = conv?.ManualCompactionCount ?? 0;
|
||||||
|
_sessionCompactionSavedTokens = conv?.CompactionSavedTokens ?? 0;
|
||||||
|
_sessionMemoryCompactionCount = conv?.SessionMemoryCompactionCount ?? 0;
|
||||||
|
_sessionMicrocompactBoundaryCount = conv?.MicrocompactBoundaryCount ?? 0;
|
||||||
|
_sessionSnipCompactionCount = conv?.SnipCompactionCount ?? 0;
|
||||||
|
_lastCompactionAt = conv?.LastCompactionAt;
|
||||||
|
_lastCompactionWasAutomatic = conv?.LastCompactionWasAutomatic ?? false;
|
||||||
|
_lastCompactionBeforeTokens = conv?.LastCompactionBeforeTokens;
|
||||||
|
_lastCompactionAfterTokens = conv?.LastCompactionAfterTokens;
|
||||||
|
_lastCompactionStageSummary = conv?.LastCompactionStageSummary ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>현재 하단 바 설정을 대화에 저장합니다.</summary>
|
/// <summary>현재 하단 바 설정을 대화에 저장합니다.</summary>
|
||||||
private void SaveConversationSettings()
|
private void SaveConversationSettings()
|
||||||
{
|
{
|
||||||
@@ -6500,9 +6528,8 @@ public partial class ChatWindow : Window
|
|||||||
ForceScrollToEnd();
|
ForceScrollToEnd();
|
||||||
|
|
||||||
var llm = _settings.Settings.Llm;
|
var llm = _settings.Settings.Llm;
|
||||||
var beforeTokens = Services.TokenEstimator.EstimateMessages(conv.Messages);
|
|
||||||
var working = conv.Messages.ToList();
|
var working = conv.Messages.ToList();
|
||||||
var condensed = await ContextCondenser.CondenseIfNeededAsync(
|
var compactResult = await ContextCondenser.CondenseWithStatsAsync(
|
||||||
working,
|
working,
|
||||||
_llm,
|
_llm,
|
||||||
llm.MaxContextTokens,
|
llm.MaxContextTokens,
|
||||||
@@ -6510,10 +6537,11 @@ public partial class ChatWindow : Window
|
|||||||
llm.ContextCompactTriggerPercent,
|
llm.ContextCompactTriggerPercent,
|
||||||
true,
|
true,
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
var afterTokens = Services.TokenEstimator.EstimateMessages(working);
|
var beforeTokens = compactResult.BeforeTokens;
|
||||||
RecordCompactionStats(beforeTokens, afterTokens, wasAutomatic: false);
|
var afterTokens = compactResult.AfterTokens;
|
||||||
|
RecordCompactionStats(compactResult, wasAutomatic: false);
|
||||||
|
|
||||||
if (condensed)
|
if (compactResult.Changed)
|
||||||
{
|
{
|
||||||
lock (_convLock)
|
lock (_convLock)
|
||||||
{
|
{
|
||||||
@@ -6521,8 +6549,8 @@ public partial class ChatWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var assistantText = condensed
|
var assistantText = compactResult.Changed
|
||||||
? $"컨텍스트 압축을 수행했습니다. 입력 토큰 추정치: {beforeTokens:N0} → {afterTokens:N0}"
|
? $"컨텍스트 압축을 수행했습니다. 입력 토큰 추정치: {beforeTokens:N0} → {afterTokens:N0}\n- 단계: {compactResult.StageSummary}\n- 절감량: {compactResult.SavedTokens:N0} tokens"
|
||||||
: "현재 대화는 압축할 충분한 이전 컨텍스트가 없어 변경 없이 유지했습니다.";
|
: "현재 대화는 압축할 충분한 이전 컨텍스트가 없어 변경 없이 유지했습니다.";
|
||||||
|
|
||||||
var assistantMsg = new ChatMessage { Role = "assistant", Content = assistantText };
|
var assistantMsg = new ChatMessage { Role = "assistant", Content = assistantText };
|
||||||
@@ -6547,7 +6575,7 @@ public partial class ChatWindow : Window
|
|||||||
ForceScrollToEnd();
|
ForceScrollToEnd();
|
||||||
if (StatusTokens != null)
|
if (StatusTokens != null)
|
||||||
StatusTokens.Text = $"컨텍스트 {Services.TokenEstimator.Format(beforeTokens)} → {Services.TokenEstimator.Format(afterTokens)}";
|
StatusTokens.Text = $"컨텍스트 {Services.TokenEstimator.Format(beforeTokens)} → {Services.TokenEstimator.Format(afterTokens)}";
|
||||||
SetStatus(condensed ? "컨텍스트 압축 완료" : "압축할 컨텍스트 없음", spinning: false);
|
SetStatus(compactResult.Changed ? "컨텍스트 압축 완료" : "압축할 컨텍스트 없음", spinning: false);
|
||||||
RefreshContextUsageVisual();
|
RefreshContextUsageVisual();
|
||||||
RefreshConversationList();
|
RefreshConversationList();
|
||||||
UpdateTaskSummaryIndicators();
|
UpdateTaskSummaryIndicators();
|
||||||
@@ -8081,8 +8109,7 @@ public partial class ChatWindow : Window
|
|||||||
// ── 전송 전 컨텍스트 사전 압축 ──
|
// ── 전송 전 컨텍스트 사전 압축 ──
|
||||||
{
|
{
|
||||||
var llm = _settings.Settings.Llm;
|
var llm = _settings.Settings.Llm;
|
||||||
var beforeCompactTokens = Services.TokenEstimator.EstimateMessages(sendMessages);
|
var compactResult = await ContextCondenser.CondenseWithStatsAsync(
|
||||||
var condensed = await ContextCondenser.CondenseIfNeededAsync(
|
|
||||||
sendMessages,
|
sendMessages,
|
||||||
_llm,
|
_llm,
|
||||||
llm.MaxContextTokens,
|
llm.MaxContextTokens,
|
||||||
@@ -8090,11 +8117,10 @@ public partial class ChatWindow : Window
|
|||||||
llm.ContextCompactTriggerPercent,
|
llm.ContextCompactTriggerPercent,
|
||||||
false,
|
false,
|
||||||
_streamCts!.Token);
|
_streamCts!.Token);
|
||||||
if (condensed)
|
if (compactResult.Changed)
|
||||||
{
|
{
|
||||||
var afterCompactTokens = Services.TokenEstimator.EstimateMessages(sendMessages);
|
RecordCompactionStats(compactResult, wasAutomatic: true);
|
||||||
RecordCompactionStats(beforeCompactTokens, afterCompactTokens, wasAutomatic: true);
|
SetStatus($"컨텍스트를 사전 정리했습니다 · {compactResult.BeforeTokens:N0} → {compactResult.AfterTokens:N0} tokens", spinning: true);
|
||||||
SetStatus("컨텍스트를 사전 정리했습니다", spinning: true);
|
|
||||||
RefreshContextUsageVisual();
|
RefreshContextUsageVisual();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16497,7 +16523,14 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
|
|||||||
? $"\n최근 압축: {(_lastCompactionWasAutomatic ? "자동" : "수동")} · {_lastCompactionAt.Value:HH:mm:ss}\n" +
|
? $"\n최근 압축: {(_lastCompactionWasAutomatic ? "자동" : "수동")} · {_lastCompactionAt.Value:HH:mm:ss}\n" +
|
||||||
$"절감: {_lastCompactionBeforeTokens.Value:N0} → {_lastCompactionAfterTokens.Value:N0} tokens " +
|
$"절감: {_lastCompactionBeforeTokens.Value:N0} → {_lastCompactionAfterTokens.Value:N0} tokens " +
|
||||||
$"(-{Math.Max(0, _lastCompactionBeforeTokens.Value - _lastCompactionAfterTokens.Value):N0}, " +
|
$"(-{Math.Max(0, _lastCompactionBeforeTokens.Value - _lastCompactionAfterTokens.Value):N0}, " +
|
||||||
$"{Services.TokenEstimator.Format(_lastCompactionBeforeTokens.Value)} → {Services.TokenEstimator.Format(_lastCompactionAfterTokens.Value)})"
|
$"{Services.TokenEstimator.Format(_lastCompactionBeforeTokens.Value)} → {Services.TokenEstimator.Format(_lastCompactionAfterTokens.Value)})\n" +
|
||||||
|
$"단계: {(!string.IsNullOrWhiteSpace(_lastCompactionStageSummary) ? _lastCompactionStageSummary : "기본")}"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
var compactSession = _sessionCompactionCount > 0
|
||||||
|
? $"\n세션 누적: {_sessionCompactionCount:N0}회 (자동 {_sessionAutomaticCompactionCount:N0} / 수동 {_sessionManualCompactionCount:N0})\n" +
|
||||||
|
$"세션 절감: {Services.TokenEstimator.Format(_sessionCompactionSavedTokens)} tokens\n" +
|
||||||
|
$"세션 메모리 {_sessionMemoryCompactionCount:N0}회 · 경계 {_sessionMicrocompactBoundaryCount:N0}건 · snip {_sessionSnipCompactionCount:N0}건"
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
TokenUsageCard.ToolTip =
|
TokenUsageCard.ToolTip =
|
||||||
@@ -16506,7 +16539,8 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
|
|||||||
$"간단 표기: {Services.TokenEstimator.Format(currentTokens)} / {Services.TokenEstimator.Format(maxContextTokens)}\n" +
|
$"간단 표기: {Services.TokenEstimator.Format(currentTokens)} / {Services.TokenEstimator.Format(maxContextTokens)}\n" +
|
||||||
$"자동 압축 시작: {triggerPercent}%\n" +
|
$"자동 압축 시작: {triggerPercent}%\n" +
|
||||||
$"현재 입력 초안 포함" +
|
$"현재 입력 초안 포함" +
|
||||||
compactHistory;
|
compactHistory +
|
||||||
|
compactSession;
|
||||||
|
|
||||||
UpdateCircularUsageArc(TokenUsageArc, usageRatio, 18, 18, 14);
|
UpdateCircularUsageArc(TokenUsageArc, usageRatio, 18, 18, 14);
|
||||||
PositionThresholdMarker(TokenUsageThresholdMarker, triggerRatio, 18, 18, 14, 3);
|
PositionThresholdMarker(TokenUsageThresholdMarker, triggerRatio, 18, 18, 14, 3);
|
||||||
@@ -16555,12 +16589,45 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
|
|||||||
centerY + radius * Math.Sin(radians));
|
centerY + radius * Math.Sin(radians));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RecordCompactionStats(int beforeTokens, int afterTokens, bool wasAutomatic)
|
private void RecordCompactionStats(ContextCompactionResult result, bool wasAutomatic)
|
||||||
{
|
{
|
||||||
_lastCompactionBeforeTokens = Math.Max(0, beforeTokens);
|
var beforeTokens = Math.Max(0, result.BeforeTokens);
|
||||||
_lastCompactionAfterTokens = Math.Max(0, afterTokens);
|
var afterTokens = Math.Max(0, result.AfterTokens);
|
||||||
|
var savedTokens = Math.Max(0, beforeTokens - afterTokens);
|
||||||
|
|
||||||
|
_lastCompactionBeforeTokens = beforeTokens;
|
||||||
|
_lastCompactionAfterTokens = afterTokens;
|
||||||
_lastCompactionAt = DateTime.Now;
|
_lastCompactionAt = DateTime.Now;
|
||||||
_lastCompactionWasAutomatic = wasAutomatic;
|
_lastCompactionWasAutomatic = wasAutomatic;
|
||||||
|
_lastCompactionStageSummary = result.StageSummary;
|
||||||
|
_sessionCompactionCount++;
|
||||||
|
_sessionCompactionSavedTokens += savedTokens;
|
||||||
|
_sessionMemoryCompactionCount += result.SessionMemoryApplied ? 1 : 0;
|
||||||
|
_sessionMicrocompactBoundaryCount += result.MicrocompactBoundaryCount;
|
||||||
|
_sessionSnipCompactionCount += result.SnippedMessageCount + result.CollapsedBoundaryCount;
|
||||||
|
if (wasAutomatic)
|
||||||
|
_sessionAutomaticCompactionCount++;
|
||||||
|
else
|
||||||
|
_sessionManualCompactionCount++;
|
||||||
|
|
||||||
|
ChatConversation? conv;
|
||||||
|
lock (_convLock) conv = _currentConversation;
|
||||||
|
if (conv == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
conv.CompactionCount = _sessionCompactionCount;
|
||||||
|
conv.AutomaticCompactionCount = _sessionAutomaticCompactionCount;
|
||||||
|
conv.ManualCompactionCount = _sessionManualCompactionCount;
|
||||||
|
conv.CompactionSavedTokens = _sessionCompactionSavedTokens;
|
||||||
|
conv.SessionMemoryCompactionCount = _sessionMemoryCompactionCount;
|
||||||
|
conv.MicrocompactBoundaryCount = _sessionMicrocompactBoundaryCount;
|
||||||
|
conv.SnipCompactionCount = _sessionSnipCompactionCount;
|
||||||
|
conv.LastCompactionAt = _lastCompactionAt;
|
||||||
|
conv.LastCompactionWasAutomatic = wasAutomatic;
|
||||||
|
conv.LastCompactionBeforeTokens = beforeTokens;
|
||||||
|
conv.LastCompactionAfterTokens = afterTokens;
|
||||||
|
conv.LastCompactionStageSummary = _lastCompactionStageSummary;
|
||||||
|
try { _storage.Save(conv); } catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ScheduleGitBranchRefresh(int delayMs = 400)
|
private void ScheduleGitBranchRefresh(int delayMs = 400)
|
||||||
|
|||||||
Reference in New Issue
Block a user