From abc355c4519c6d47579db3af3482c6b51e90b53f Mon Sep 17 00:00:00 2001 From: lacvet Date: Sun, 5 Apr 2026 12:39:54 +0900 Subject: [PATCH] =?UTF-8?q?AX=20Agent=20=EC=8B=A4=ED=96=89=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EC=A0=80=EC=9E=A5=EC=9D=84=20=EB=B0=B0?= =?UTF-8?q?=EC=B9=98=ED=98=95=EC=9C=BC=EB=A1=9C=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - execution event와 agent run 기록이 들어올 때마다 즉시 저장하지 않고 conversationPersistTimer로 220ms 단위 지연 저장하도록 변경함 - Cowork/Code 연속 이벤트 구간의 디스크 I/O를 줄여 체감 끊김과 잦은 저장 부하를 낮추는 방향으로 정리함 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0 --- README.md | 2 ++ docs/DEVELOPMENT.md | 3 ++ src/AxCopilot/Views/ChatWindow.xaml.cs | 47 +++++++++++++++++++++++--- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1e68205..412041b 100644 --- a/README.md +++ b/README.md @@ -744,6 +744,7 @@ ow + toggle 시각 언어로 통일했습니다. - 이어서 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 실행 후처리도 `ResetStreamingUiState()`, `FinalizeConversationTurn()`, `FinalizeQueuedDraft()`로 묶었습니다. 전송과 재생성이 같은 정리 경로를 공유하게 해서, 응답 완료 뒤 상태 복구와 대화 저장, 대기열 완료/실패 처리 흐름도 더 한 축으로 정리했습니다. - 이번엔 `OnAgentEvent(...)`의 본문 재렌더를 배치형으로 바꿨습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 `DispatcherTimer` 기반 `ScheduleExecutionHistoryRender()`를 추가해서, Cowork/Code 실행 중 이벤트가 연속으로 들어와도 `RenderMessages()`가 매 이벤트마다 바로 돌지 않고 짧게 묶여 한 번씩만 반영됩니다. - 같은 흐름으로 작업 요약 스트립도 배치형 갱신으로 바꿨습니다. `UpdateTaskSummaryIndicators()`를 즉시 호출하는 대신 `ScheduleTaskSummaryRefresh()`가 120ms 단위로 상태 반영을 묶어, 실행 중 상단 상태 스트립과 런타임 배지가 과하게 흔들리지 않도록 정리했습니다. +- 추가로 실행 이벤트/실행 기록 저장도 지연 저장으로 바꿨습니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)의 `AppendConversationExecutionEvent()`와 `AppendConversationAgentRun()`은 이제 이벤트마다 바로 `_storage.Save(...)`를 호출하지 않고, `ScheduleConversationPersist()`를 통해 220ms 단위로 묶어서 flush 합니다. Cowork/Code의 연속 이벤트 구간에서 저장 I/O가 덜 붙도록 만든 조정입니다. - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\` 경고 0 / 오류 0 - 업데이트: 2026-04-05 12:24 (KST) - 업데이트: 2026-04-05 12:31 (KST) @@ -752,6 +753,7 @@ ow + toggle 시각 언어로 통일했습니다. - 업데이트: 2026-04-05 12:47 (KST) - 업데이트: 2026-04-05 12:53 (KST) - 업데이트: 2026-04-05 12:58 (KST) +- 업데이트: 2026-04-05 13:03 (KST) --- diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 5062887..b25258f 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4507,4 +4507,7 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - 업데이트: 2026-04-05 12:58 (KST) - 같은 파일에 `_taskSummaryRefreshTimer`와 `ScheduleTaskSummaryRefresh()`도 추가해, `UpdateTaskSummaryIndicators()`가 실행 이벤트나 서브에이전트 상태 변경마다 즉시 도는 대신 120ms 단위로 묶여 반영되게 했습니다. - 이 조정은 `RuntimeActivityBadge`, `ConversationStatusStrip`, 완료 요약 라벨 같은 상태 스트립이 빠르게 깜빡이는 체감을 줄이기 위한 것이며, 본문 재렌더 배치와 함께 Cowork/Code 실행 중 전체 UI 안정성을 높이는 단계입니다. +- 업데이트: 2026-04-05 13:03 (KST) +- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)에 `_conversationPersistTimer`, `ScheduleConversationPersist()`, `FlushPendingConversationPersists()`를 추가해, 실행 이벤트와 실행 기록이 들어올 때마다 곧바로 `_storage.Save(...)`를 치지 않도록 바꿨습니다. +- `AppendConversationExecutionEvent()`와 `AppendConversationAgentRun()`는 이제 `ChatSessionStateService`의 append 메서드를 `storage=null`로 호출하고, 변경된 `ChatConversation`만 220ms 단위로 지연 저장합니다. 이 변경은 Cowork/Code 실행 중 빈번한 이벤트가 들어올 때 디스크 I/O 때문에 체감이 끊기는 문제를 줄이기 위한 배치 저장 단계입니다. - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\` 경고 0 / 오류 0 diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 07345a6..a6324b2 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -85,6 +85,7 @@ public partial class ChatWindow : Window private readonly DispatcherTimer _inputUiRefreshTimer; private readonly DispatcherTimer _executionHistoryRenderTimer; private readonly DispatcherTimer _taskSummaryRefreshTimer; + private readonly DispatcherTimer _conversationPersistTimer; private CancellationTokenSource? _gitStatusRefreshCts; private int _displayedLength; // 현재 화면에 표시된 글자 수 private ResourceDictionary? _agentThemeDictionary; @@ -119,6 +120,7 @@ public partial class ChatWindow : Window private int _sessionPostCompactionPromptTokens; private int _sessionPostCompactionCompletionTokens; private bool _pendingExecutionHistoryAutoScroll; + private readonly Dictionary _pendingConversationPersists = new(StringComparer.OrdinalIgnoreCase); private void ApplyQuickActionVisual(Button button, bool active, string activeBg, string activeFg) { if (button?.Content is not string text) @@ -251,6 +253,12 @@ public partial class ChatWindow : Window _taskSummaryRefreshTimer.Stop(); UpdateTaskSummaryIndicators(); }; + _conversationPersistTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(220) }; + _conversationPersistTimer.Tick += (_, _) => + { + _conversationPersistTimer.Stop(); + FlushPendingConversationPersists(); + }; KeyDown += ChatWindow_KeyDown; UpdateConversationFailureFilterUi(); @@ -8557,6 +8565,7 @@ public partial class ChatWindow : Window _executionHistoryRenderTimer.Stop(); _pendingExecutionHistoryAutoScroll = false; _taskSummaryRefreshTimer.Stop(); + _conversationPersistTimer.Stop(); HideStickyProgress(); StopRainbowGlow(); _activeStreamText = null; @@ -8626,6 +8635,30 @@ public partial class ChatWindow : Window _taskSummaryRefreshTimer.Start(); } + private void ScheduleConversationPersist(ChatConversation? conversation) + { + if (conversation == null || string.IsNullOrWhiteSpace(conversation.Id)) + return; + + _pendingConversationPersists[conversation.Id] = conversation; + _conversationPersistTimer.Stop(); + _conversationPersistTimer.Start(); + } + + private void FlushPendingConversationPersists() + { + if (_pendingConversationPersists.Count == 0) + return; + + foreach (var conversation in _pendingConversationPersists.Values.ToList()) + { + try { _storage.Save(conversation); } + catch (Exception ex) { Services.LogService.Debug($"대화 지연 저장 실패: {ex.Message}"); } + } + + _pendingConversationPersists.Clear(); + } + // ─── 코워크 에이전트 지원 ──────────────────────────────────────────── private string BuildCoworkSystemPrompt() @@ -9106,15 +9139,18 @@ public partial class ChatWindow : Window var normalizedTarget = NormalizeTabName(targetTab); var normalizedActive = NormalizeTabName(_activeTab); + ChatConversation updatedConversation; if (string.Equals(normalizedTarget, normalizedActive, StringComparison.OrdinalIgnoreCase)) { - _currentConversation = session.AppendAgentRun(normalizedTarget, evt, status, summary, _storage); + _currentConversation = updatedConversation = session.AppendAgentRun(normalizedTarget, evt, status, summary, null); + ScheduleConversationPersist(updatedConversation); return; } var activeSnapshot = _currentConversation; var previousSessionConversation = session.CurrentConversation; - session.AppendAgentRun(normalizedTarget, evt, status, summary, _storage); + updatedConversation = session.AppendAgentRun(normalizedTarget, evt, status, summary, null); + ScheduleConversationPersist(updatedConversation); if (activeSnapshot != null && string.Equals(NormalizeTabName(activeSnapshot.Tab), normalizedActive, StringComparison.OrdinalIgnoreCase)) { @@ -9156,15 +9192,18 @@ public partial class ChatWindow : Window var normalizedTarget = NormalizeTabName(targetTab); var normalizedActive = NormalizeTabName(_activeTab); + ChatConversation updatedConversation; if (string.Equals(normalizedTarget, normalizedActive, StringComparison.OrdinalIgnoreCase)) { - _currentConversation = session.AppendExecutionEvent(normalizedTarget, evt, _storage); + _currentConversation = updatedConversation = session.AppendExecutionEvent(normalizedTarget, evt, null); + ScheduleConversationPersist(updatedConversation); return; } var activeSnapshot = _currentConversation; var previousSessionConversation = session.CurrentConversation; - session.AppendExecutionEvent(normalizedTarget, evt, _storage); + updatedConversation = session.AppendExecutionEvent(normalizedTarget, evt, null); + ScheduleConversationPersist(updatedConversation); if (activeSnapshot != null && string.Equals(NormalizeTabName(activeSnapshot.Tab), normalizedActive, StringComparison.OrdinalIgnoreCase)) {