코워크·코드 장시간 실행 종료 직전 NullReference 실패를 방지하고 예외 추적을 보강한다
Some checks are pending
Release Gate / gate (push) Waiting to run

- ChatWindow 실행 경로에서 스트리밍 취소 토큰을 지역 변수로 고정해 마지막 라이브 프리뷰 단계에서 _streamCts null 참조가 발생하지 않도록 수정

- 최종 타이핑 프리뷰 컨테이너 준비 실패와 취소 예외를 방어적으로 처리해 장시간 작업이 마지막 UI 렌더 때문에 오류로 뒤집히지 않도록 정리

- 에이전트 실행 예외 전체를 앱 로그에 남기고 README 및 DEVELOPMENT 문서 이력을 갱신
This commit is contained in:
2026-04-07 09:26:28 +09:00
parent f34878cbd5
commit 4c8b550242
3 changed files with 26 additions and 6 deletions

View File

@@ -1466,3 +1466,6 @@ MIT License
- 업데이트: 2026-04-07 03:13 (KST) - 업데이트: 2026-04-07 03:13 (KST)
- Cowork/Code 실행 중 탭을 바꿀 때 작업을 즉시 취소하던 흐름을 제거했습니다. 이제 실행은 시작한 탭에서 계속 진행되고, 다른 탭으로 이동해도 작업이 사용자 취소처럼 끝나지 않습니다. - Cowork/Code 실행 중 탭을 바꿀 때 작업을 즉시 취소하던 흐름을 제거했습니다. 이제 실행은 시작한 탭에서 계속 진행되고, 다른 탭으로 이동해도 작업이 사용자 취소처럼 끝나지 않습니다.
- 라이브 진행 힌트는 실행을 시작한 탭에서만 보이도록 조정해, Cowork 작업 중 Code 탭으로 이동했을 때 Code 쪽 transcript에 `처리 중...`이 따라 보이던 상태 오염을 막았습니다. - 라이브 진행 힌트는 실행을 시작한 탭에서만 보이도록 조정해, Cowork 작업 중 Code 탭으로 이동했을 때 Code 쪽 transcript에 `처리 중...`이 따라 보이던 상태 오염을 막았습니다.
- 업데이트: 2026-04-07 09:32 (KST)
- Cowork/Code 장시간 실행 뒤 마지막 응답을 라이브 프리뷰로 붙이는 단계에서 `_streamCts` 필드를 다시 읽다가 `Object reference not set to an instance of an object.`로 실패할 수 있던 경로를 수정했습니다. 이제 실행 시작 시 캡처한 지역 토큰을 끝까지 재사용하고, 최종 타이핑 프리뷰 컨테이너가 준비되지 않으면 조용히 건너뛰도록 방어 로직을 추가했습니다.
- 같은 실패가 다시 생기면 원인을 더 빨리 찾을 수 있도록 AX Agent 실행 예외 전체를 앱 로그에 남기도록 보강했습니다.

View File

@@ -5324,6 +5324,10 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
- Chat/Cowork/Code 탭 전환 시 실행 중 작업을 즉시 취소하던 `StopStreamingIfActive()` 호출을 제거했다. 이제 Cowork/Code 작업은 시작한 탭 기준으로 백그라운드에서 계속 진행된다. - Chat/Cowork/Code 탭 전환 시 실행 중 작업을 즉시 취소하던 `StopStreamingIfActive()` 호출을 제거했다. 이제 Cowork/Code 작업은 시작한 탭 기준으로 백그라운드에서 계속 진행된다.
- 실행 중 컨트롤 표시를 `RefreshStreamingControlsForActiveTab()`로 분리해, 현재 탭이 실행 소유 탭일 때만 정지/일시정지 버튼이 보이고 다른 탭에서는 일반 입력 상태처럼 보이도록 정리했다. - 실행 중 컨트롤 표시를 `RefreshStreamingControlsForActiveTab()`로 분리해, 현재 탭이 실행 소유 탭일 때만 정지/일시정지 버튼이 보이고 다른 탭에서는 일반 입력 상태처럼 보이도록 정리했다.
- 라이브 진행 힌트는 `_streamRunTab`과 현재 활성 탭이 일치할 때만 transcript에 렌더되도록 바꿔, Cowork 작업 중 Code 탭으로 이동했을 때 Code 탭에도 `처리 중...`이 따라 보이던 문제를 막았다. - 라이브 진행 힌트는 `_streamRunTab`과 현재 활성 탭이 일치할 때만 transcript에 렌더되도록 바꿔, Cowork 작업 중 Code 탭으로 이동했을 때 Code 탭에도 `처리 중...`이 따라 보이던 문제를 막았다.
- [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)
- Cowork/Code 장시간 실행 뒤 최종 응답을 `ShowTypedAssistantPreviewAsync(...)`로 붙이는 단계에서 `_streamCts` 필드를 다시 읽다가 null 참조로 실패할 수 있던 경로를 수정했다. 실행 시작 시 만든 `CancellationTokenSource`의 토큰을 지역 변수로 고정해 마지막 프리뷰 단계까지 재사용하도록 변경했다.
- 최종 타이핑 프리뷰 컨테이너가 정상적으로 준비되지 않은 경우 조용히 건너뛰도록 방어 로직을 추가했고, 타이핑 대기 중 취소가 들어오면 `OperationCanceledException`을 내부에서 흡수해 최종 실패로 뒤집히지 않도록 정리했다.
- 에이전트 실행 중 예외 전체를 `Services.LogService.Debug(...)`에 남겨, 이후 장시간 실행 실패 원인을 앱 로그에서 바로 추적할 수 있게 했다.
- Document update: 2026-04-07 09:19 (KST) - Restored the AX Agent footer/status total token aggregate so it no longer disappears after runs return to idle. The status strip now rehydrates totals from the current conversation message token sums when live loop counters are empty. - Document update: 2026-04-07 09:19 (KST) - Restored the AX Agent footer/status total token aggregate so it no longer disappears after runs return to idle. The status strip now rehydrates totals from the current conversation message token sums when live loop counters are empty.
- Document update: 2026-04-07 09:19 (KST) - Corrected context-compaction popup accuracy by switching its detail copy to the last real compaction metrics (`before -> after`, automatic/manual kind, cumulative compaction count, cumulative saved tokens) instead of only the generic trigger-threshold text. - Document update: 2026-04-07 09:19 (KST) - Corrected context-compaction popup accuracy by switching its detail copy to the last real compaction metrics (`before -> after`, automatic/manual kind, cumulative compaction count, cumulative saved tokens) instead of only the generic trigger-threshold text.
- Document update: 2026-04-07 09:19 (KST) - Prevented `total_stats` loop events from being swallowed into the generic process-feed path. AX Agent now routes those events back through the dedicated total-stats presentation so transcript summaries and footer token totals stay aligned. - Document update: 2026-04-07 09:19 (KST) - Prevented `total_stats` loop events from being swallowed into the generic process-feed path. AX Agent now routes those events back through the dedicated total-stats presentation so transcript summaries and footer token totals stay aligned.

View File

@@ -5595,7 +5595,9 @@ public partial class ChatWindow : Window
BtnStop.Visibility = Visibility.Visible; BtnStop.Visibility = Visibility.Visible;
if (runTab == "Cowork" || runTab == "Code") if (runTab == "Cowork" || runTab == "Code")
BtnPause.Visibility = Visibility.Visible; BtnPause.Visibility = Visibility.Visible;
_streamCts = new CancellationTokenSource(); var streamCts = new CancellationTokenSource();
_streamCts = streamCts;
var streamToken = streamCts.Token;
ForceScrollToEnd(); ForceScrollToEnd();
var assistantContent = string.Empty; var assistantContent = string.Empty;
@@ -5651,8 +5653,8 @@ public partial class ChatWindow : Window
preparedExecution, preparedExecution,
(messages, token) => RunAgentLoopAsync(runTab, rememberTab, conversation, messages, token), (messages, token) => RunAgentLoopAsync(runTab, rememberTab, conversation, messages, token),
(messages, token) => _llm.SendAsync(messages.ToList(), token), (messages, token) => _llm.SendAsync(messages.ToList(), token),
_streamCts.Token); streamToken);
assistantContent = response; assistantContent = response ?? string.Empty;
} }
responseElapsedMs = Math.Max(0, (long)(DateTime.UtcNow - _streamStartTime).TotalMilliseconds); responseElapsedMs = Math.Max(0, (long)(DateTime.UtcNow - _streamStartTime).TotalMilliseconds);
@@ -5681,7 +5683,7 @@ public partial class ChatWindow : Window
} }
else if (preparedExecution.Mode.UseAgentLoop && !string.IsNullOrWhiteSpace(assistantContent)) else if (preparedExecution.Mode.UseAgentLoop && !string.IsNullOrWhiteSpace(assistantContent))
{ {
await ShowTypedAssistantPreviewAsync(assistantContent, _streamCts.Token); await ShowTypedAssistantPreviewAsync(assistantContent, streamToken);
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -5695,6 +5697,7 @@ public partial class ChatWindow : Window
} }
catch (Exception ex) catch (Exception ex)
{ {
Services.LogService.Debug($"에이전트 실행 실패: {ex}");
var finalized = _chatEngine.FinalizeExecutionContentForUi(assistantContent, ex); var finalized = _chatEngine.FinalizeExecutionContentForUi(assistantContent, ex);
assistantContent = finalized.Content; assistantContent = finalized.Content;
draftFailure = finalized.FailureReason; draftFailure = finalized.FailureReason;
@@ -5734,6 +5737,9 @@ public partial class ChatWindow : Window
return; return;
var container = CreateStreamingContainer(out var streamText); var container = CreateStreamingContainer(out var streamText);
if (container == null || streamText == null || MessagePanel == null)
return;
_activeStreamText = streamText; _activeStreamText = streamText;
_cachedStreamContent = finalContent; _cachedStreamContent = finalContent;
_displayedLength = 0; _displayedLength = 0;
@@ -5745,8 +5751,15 @@ public partial class ChatWindow : Window
streamText.Text = _cursorVisible ? "\u258c" : " "; streamText.Text = _cursorVisible ? "\u258c" : " ";
var deadline = DateTime.UtcNow.AddMilliseconds(Math.Clamp(finalContent.Length * 6, 500, 1800)); var deadline = DateTime.UtcNow.AddMilliseconds(Math.Clamp(finalContent.Length * 6, 500, 1800));
while (_displayedLength < _cachedStreamContent.Length && DateTime.UtcNow < deadline && !ct.IsCancellationRequested) try
await Task.Delay(30, ct); {
while (_displayedLength < _cachedStreamContent.Length && DateTime.UtcNow < deadline && !ct.IsCancellationRequested)
await Task.Delay(30, ct);
}
catch (OperationCanceledException)
{
return;
}
_displayedLength = _cachedStreamContent.Length; _displayedLength = _cachedStreamContent.Length;
if (_activeStreamText != null) if (_activeStreamText != null)