Theme / v13.24.1

Claude-Mem

Claude Code 持久化記憶系統

實戰範例

實戰範例 001:利用 Claude-Mem 記憶鏈排查 CI/CD 隨機測試失敗

實戰演練如何利用 Claude-Mem 的會話時間軸與歷史日誌,追蹤並解決在 GitHub Actions 中隨機出現的併發死鎖測試 Bug。

實戰背景

在自動化軟體建置(CI/CD)流程中,最讓人頭疼的就是 隨機性測試失敗 (Flaky Tests)。這些測試在本地執行時 100% 通過,但在 GitHub Actions 等雲端環境下,因為 CPU 核數不同或網路延遲,每跑 10 次就會有 1 次莫名崩潰。

痛點與問題描述: 我們的後端 API 整合測試 test_plc_reconnect_loop 在 CI 建置中隨機失敗。每次失敗的日誌都散落於不同的 GitHub Actions 運行記錄(Run History)中。新接手任務的 AI 助手每次看見這個錯誤,都會把它當成一個「全新的偶發問題」,並給予簡單的「重試即可」建議,完全無法感知這其實是一個已經發生了 5 次的頑固性併發 Bug

本範例將展示如何利用 Claude-Memtimeline 與持久化歷史日誌,串聯起多次 CI 失敗的特徵,引導 AI 助手進行科學根因分析並徹底修復。


規格定義

本次變更計畫的 ID 為 fix-flaky-ci-test。我們在 OpenSpec 中定義了以下驗收標準:

# 實戰範例 001:openspec/changes/fix-flaky-ci-test/specs/ci-reliability/spec.md

## ADDED/MODIFIED Requirements

### Requirement: Thread-safe reconnection polling
PLC 重連監控輪詢器必須(SHALL)以線程安全方式管理計時器。
- 嚴禁(SHALL NOT)在 `Dispose` 方法與計時器回呼中產生嵌套鎖定。

#### Scenario: Verify parallel CI execution
- **WHEN** 執行全量並行整合測試 50 次以上
- **THEN** 所有測試必須(SHALL)100% 通過,無任何死鎖與執行緒掛起發生。

端到端 8 輪 AI 協作紀錄

以下是 You (開發者) 與 AI 助手在排查此 Flaky Test 時的完整對話歷史。

── 第 1 輪:開啟會話與歷史 CI 錯誤檢索 ──

  • You:
    /opsx:explore "我們的 test_plc_reconnect_loop 測試又在 CI 中失敗了。請幫我查詢 timeline,看看我們之前有沒有紀錄過這個 Flaky Test 的報錯特徵。"
  • AI:

    好的,已進入探索模式。為了解決這個 Flaky Test,我將調用 claude-memtimeline 工具,檢索歷史 Session 中是否有與 “test_plc_reconnect_loop” 或 “Flaky Test” 相關的日誌與觀察。

    CM 呼叫: timeline(query: "test_plc_reconnect_loop")
    CM 回傳:

    [
      {
        "sessionDate": "2026-07-15T14:22:00Z",
        "summary": "CI 失敗:test_plc_reconnect_loop 發生 TaskCanceledException。臨時處置:將 Timeout 延長,但未徹底解決。",
        "observationId": "obs_0891"
      },
      {
        "sessionDate": "2026-07-16T08:30:00Z",
        "summary": "CI 失敗:test_plc_reconnect_loop 逾時掛起(Hung)。線程轉儲顯示在 ReconnectionMonitor.cs 第 42 行有 Lock 競爭。",
        "observationId": "obs_0923"
      }
    ]

    分析
    時間軸揭示了這不是一次偶發事件!

    1. 第一次失敗(昨日):發生了 TaskCanceledException,當時僅做了延長超時的臨時處理。
    2. 第二次失敗(今晨):測試直接掛起(Hung),線程轉儲定位在 ReconnectionMonitor.cs 的第 42 行有鎖競爭。

    接下來,我需要讀取 ReconnectionMonitor.cs 的第 42 行。


── 第 2 輪:定位鎖競爭代碼 ──

  • You:
    使用 get_code_snippet 讀取 ReconnectionMonitor.cs 中第 42 行及其周邊原始碼。
  • AI:

    我呼叫 get_code_snippet 讀取該類別中受影響的代碼區間。

    CM 呼叫: get_code_snippet(qualifiedName: "ControlSystem.Core.Jobs.ReconnectionMonitor")
    CM 回傳:

    • 檔案路徑: /src/Core/Jobs/ReconnectionMonitor.cs (第 35 行到第 50 行)
    • 原始碼:
      private readonly object _lockObj = new object();
      
      public void StartMonitor()
      {
          lock (_lockObj) // 第 42 行
          {
              if (_timer == null) {
                  _timer = new Timer(OnTimerTick, null, 0, 1000);
              }
          }
      }

    分析
    這裡使用了一個 lock (_lockObj) 來同步計時器的啟動。我們還需要看看 OnTimerTick 的實作,因為死鎖通常發生在定時器回呼執行緒嘗試重新獲取同一個鎖,或者是 Dispose 時。


── 第 3 輪:查看計時器回呼與 Dispose 實作 ──

  • You:
    使用 get_code_snippet 讀取 ReconnectionMonitor 中的 OnTimerTick 與 Dispose 方法。
  • AI:

    好的,我拉取這兩個方法的原始碼。

    CM 呼叫: get_code_snippet(qualifiedName: "ControlSystem.Core.Jobs.ReconnectionMonitor.Dispose")
    CM 回傳:

    public void Dispose()
    {
        lock (_lockObj) // Dispose 執行緒嘗試獲取鎖
        {
            _timer?.Dispose();
        }
    }
    
    private void OnTimerTick(object state)
    {
        lock (_lockObj) // 定時器回呼執行緒嘗試獲取鎖
        {
            // 執行重連檢查...
        }
    }

    死鎖根因診斷
    找到了!這是一個非常經典的 .NET Timer 死鎖 (Timer Deadlock)。 當 Dispose 被呼叫時,它持有 _lockObj 並調用 _timer.Dispose()。然而,此時如果定時器回呼 OnTimerTick 正好在背景執行緒中觸發,並在進入方法後被卡在 lock (_lockObj)。 根據 .NET 規範,_timer.Dispose() 會等待所有進行中的回呼執行完畢後才返回。於是:

    • Dispose 執行緒持有鎖,等待 OnTimerTick 執行結束。
    • OnTimerTick 執行緒等待 Dispose 釋放鎖。

    兩者在 CI 虛擬機高負載(線程調度延遲)時互卡,導致整個測試程式永久掛起!


── 第 4 輪:建立提案 ──

  • You:
    /opsx:propose "修復 ReconnectionMonitor 中的定時器死鎖,採用 Interlocked 或非阻塞計時器管理"
  • AI:

    好的,提案 fix-flaky-ci-test 已成功建立!

    • proposal.md: 說明定時器與 Dispose 死鎖的成因,並提出改用 System.Threading.Tasks.Task 異步循環或移除 Dispose 中的 Lock 來修復。
    • specs/ci-reliability/spec.md: 定義 50 次並行測試無死鎖的驗收標準。
    • tasks.md: 任務如下:
      • 1.1 修改 ReconnectionMonitor.cs 去除 Dispose 中的同步鎖,改為安全的 Interlocked 標記。
      • 1.2 撰寫測試在多執行緒下模擬併發 Dispose。
      • 1.3 歸檔。

    請輸入 /opsx:apply 開始實作。


── 第 5 輪:重構定時器實作 (Task 1.1) ──

  • You:
    /opsx:apply
  • AI:

    Implementing: fix-flaky-ci-test (schema: spec-driven)

    正在處理任務 1.1:去鎖重構。

    我已修改了 ReconnectionMonitor.cs,使用一個狀態標記並在 Dispose 中採用非阻塞方式釋放 Timer:

    private int _isDisposed = 0;
    
    public void Dispose()
    {
        if (Interlocked.Exchange(ref _isDisposed, 1) == 0)
        {
            // 移除 lock (_lockObj),安全釋放計時器而不阻塞回呼
            var timer = _timer;
            _timer = null;
            timer?.Dispose();
        }
    }
    
    private void OnTimerTick(object state)
    {
        if (Volatile.Read(ref _isDisposed) == 1) return;
        
        lock (_lockObj)
        {
            // 僅在寫入狀態時使用鎖,且與 Dispose 執行緒解耦
        }
    }

    ✓ 任務 1.1 實作完成。


── 第 6 輪:併發模擬測試與驗證 (Task 1.2) ──

  • You:
    繼續任務 1.2,在本地模擬高併發 Dispose 以驗證是否還會死鎖。
  • AI:

    Implementing: fix-flaky-ci-test

    正在處理任務 1.2 & 1.3:併發測試與驗證。

    1. 併發測試:我在測試中啟動了 10 個平行 Task,同時調用 StartMonitorDispose,並伴隨定時器高頻率觸發。
    2. 建置測試pnpm run build 通過。
    3. 運行結果:50 次併發模擬測試全部在 20 毫秒內順利完成,無任何線程掛起與死鎖!Flaky Bug 被徹底修復。

    已將變更同步至主規格並歸檔。

    Archive Complete

    變更計畫已成功歸檔!


── 第 7 輪:記錄此資深除錯記憶 ──

  • You:
    請把這個 Timer 導致死鎖的除錯經驗寫入長期記憶。這對我們專案中其他使用 Timer 的地方非常重要。
  • AI:

    好的,這是一條極具價值的併發設計實戰記憶。我將調用 save_observation 將其永久儲存。

    CM 呼叫:

    save_observation(
      key: "timer_deadlock_prevention",
      value: "在 .NET Timer 的 Dispose 中嚴禁與定時器回呼 OnTimerTick 使用同一個同步鎖,否則在高載下會引發 Task/Timer Dispose 死鎖。應改用 Interlocked 狀態標記。",
      category: "concurrency_policy"
    )

    CM 回傳: Success

    記憶寫入成功!現在該項併發規範已被持久化。未來不論是誰修改專案中的計時器邏輯,我都會主動跳出此提示,進行語法預警。


── 第 8 輪:總結 timeline 對於 Flaky Bug 的排查效益 ──

  • You:
    如果沒有 timeline,這個隨機 Bug 估計還要卡我們幾天。
  • AI:

    確實如此。排查 Flaky Bug 最難的是「無法在本地穩定重現」。

    • 傳統除錯:每次 CI 報錯,我們都只能看到當下的日誌,很難把上週二與今天早上的失敗特徵串聯起來。
    • Claude-Mem 記憶鏈:透過時間軸,我們成功將「TaskCanceledException」與「鎖競爭掛起」兩次不同現象的蛛絲馬跡關聯在一起,讓 AI 能在第一個 Turn 就推導出「Timer.Dispose 阻塞定時器回呼」的底層死鎖設計缺陷。這展現了記憶連續性在複雜併發排查中的巨大威力。