Skip to content

Add distributed tracing support to conversation history compaction - #1569

Open
aantn wants to merge 10 commits into
masterfrom
claude/show-compaction-traces-fH5c2
Open

aantn wants to merge 10 commits into
masterfrom
claude/show-compaction-traces-fH5c2

Conversation

@aantn

@aantn aantn commented Feb 15, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR adds distributed tracing instrumentation to the conversation history compaction feature, enabling better observability and debugging of the compaction process in production environments.

Key Changes

  • Added tracing to compaction workflow: Wrapped the conversation history compaction logic in limit_input_context_window() with a trace span that captures input parameters (initial tokens, max context size, compaction threshold) and output metrics (compacted tokens, compression ratio, status)
  • Added tracing to compaction LLM call: Instrumented the LLM completion call within compact_conversation_history() with a dedicated span that logs message count and completion status
  • Updated function signatures: Added optional trace_span parameter (defaulting to DummySpan()) to both limit_input_context_window() and compact_conversation_history() functions to support tracing
  • Propagated trace context: Updated tool_calling_llm.py to pass the trace span through to limit_input_context_window()
  • Added regression tests: Tagged existing compaction test cases with regression tag and added test_compaction.py to the evaluation regression test suite to ensure compaction functionality is continuously validated
  • Updated test implementation: Modified test_compaction.py to pass the trace span to compact_conversation_history()

Implementation Details

  • The tracing uses a context manager pattern (with trace_span.start_span()) to cleanly scope trace spans
  • Compression metrics (compression ratio) are calculated and logged for successful compactions
  • Failed compactions (where no token reduction is achieved) are logged with a "no_reduction" status
  • The DummySpan default allows the functions to work without tracing infrastructure while maintaining backward compatibility

https://claude.ai/code/session_01KagxBSbTp1AbDQTvbk1KL8

Summary by CodeRabbit

Release Notes

  • New Features

    • Added comprehensive tracing and observability around conversation history compaction operations.
    • Added test framework support to verify conversation history compaction occurred during execution.
  • Tests

    • Added new test cases for conversation compaction scenarios, including heavy-load Prometheus stress testing.
    • Improved test error reporting with test identifiers for easier debugging.
  • Chores

    • Updated test configuration infrastructure to support compaction verification flags.

Thread trace_span through the compaction pipeline so compaction events
appear as spans in Braintrust traces. Previously, compaction was invisible
in traces because no span was created for the compaction LLM call or the
surrounding orchestration logic.

- Add trace_span parameter to compact_conversation_history() and wrap
  the compaction LLM call in a "Compaction LLM Call" span
- Add trace_span parameter to limit_input_context_window() and wrap the
  full compaction flow in a "Conversation History Compaction" span with
  input/output logging (token counts, compression ratio, status)
- Pass trace_span from ToolCallingLLM.call() to limit_input_context_window()
- Pass compaction_span in test_compaction.py eval test

https://claude.ai/code/session_01KagxBSbTp1AbDQTvbk1KL8
Signed-off-by: Claude <noreply@anthropic.com>
Add test_compaction.py to the eval-regression workflow so compaction
tests (with their Braintrust trace spans) run in CI/CD. Tag
001_compaction and 003_cascading_failure with the regression marker
so they execute in automatic regression runs.

https://claude.ai/code/session_01KagxBSbTp1AbDQTvbk1KL8
Signed-off-by: Claude <noreply@anthropic.com>
@netlify

netlify Bot commented Feb 15, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for holmes-docs ready!

Name Link
🔨 Latest commit 92e438a
🔍 Latest deploy log https://app.netlify.com/projects/holmes-docs/deploys/69ac9bde3bca2f000879bfb0
😎 Deploy Preview https://deploy-preview-1569--holmes-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions

github-actions Bot commented Feb 15, 2026 •

Copy link
Copy Markdown
Contributor

📂 Previous Runs

📜 Run @ ebac52b (#22227862791)

✅ Results of HolmesGPT evals

Automatically triggered by commit ebac52b on branch claude/show-compaction-traces-fH5c2

View workflow logs

Results of HolmesGPT evals

  • ask_holmes: 8/12 test cases were successful, 2 regressions, 2 setup failures
Status Test case Time Turns Tools Cost
✅ 09_crashpod 38.0s 5 12 $0.2504
❌ 101_loki_historical_logs_pod_deleted 52.5s 6 14 $0.2603
✅ 111_pod_names_contain_service 39.2s 5 11 $0.2342
✅ 112_find_pvcs_by_uuid 31.4s 5 7 $0.2263
✅ 12_job_crashing 36.1s 5 11 $0.2355
❌ 161_conversation_compaction 50.9s 2 1 $0.1325
✅ 163_compaction_follow_up 102.2s 3 2 $0.1770
✅ 176_network_policy_blocking_traffic_no_runbooks 77.1s 11 25 $0.4246
🚧 217_prometheus_compaction_heavy_queries — — — —
✅ 24_misconfigured_pvc 42.4s 6 15 $0.2611
✅ 43_current_datetime_from_prompt 6.1s 1 — $0.1116
🚧 61_exact_match_counting — — — —
Total 47.6s avg 4.9 avg 10.9 avg $2.3135

⚠️ 2 Failures Detected

📜 Run @ 974433e (#22077731806)

✅ Results of HolmesGPT evals

Automatically triggered by commit 974433e on branch claude/show-compaction-traces-fH5c2

View workflow logs

Results of HolmesGPT evals

  • ask_holmes: 2/12 test cases were successful, 0 regressions, 10 setup failures
Status Test case Time Turns Tools Cost
🚧 09_crashpod — — — —
🚧 101_loki_historical_logs_pod_deleted — — — —
✅ 111_pod_names_contain_service 36.4s 6 10 $0.2267
🚧 112_find_pvcs_by_uuid — — — —
🚧 12_job_crashing — — — —
🚧 161_conversation_compaction — — — —
🚧 163_compaction_follow_up — — — —
🚧 176_network_policy_blocking_traffic_no_runbooks — — — —
🚧 217_prometheus_compaction_heavy_queries — — — —
✅ 24_misconfigured_pvc 36.0s 5 14 $0.2365
🚧 43_current_datetime_from_prompt — — — —
🚧 61_exact_match_counting — — — —
Total 36.2s avg 5.5 avg 12.0 avg $0.4633
📜 Run @ 1f03b90 (#22065898512)

✅ Results of HolmesGPT evals

Automatically triggered by commit 1f03b90 on branch claude/show-compaction-traces-fH5c2

View workflow logs

Results of HolmesGPT evals

  • ask_holmes: 9/12 test cases were successful, 0 regressions, 3 setup failures
Status Test case Time Turns Tools Cost
✅ 09_crashpod 41.0s 5 11 $0.2320
🚧 101_loki_historical_logs_pod_deleted — — — —
✅ 111_pod_names_contain_service 251.3s 6 13 $0.2563
✅ 112_find_pvcs_by_uuid 33.8s 5 7 $0.2197
✅ 12_job_crashing 65.4s 5 12 $0.2387
🚧 161_conversation_compaction — — — —
✅ 163_compaction_follow_up 150.9s 3 2 $0.1773
✅ 176_network_policy_blocking_traffic_no_runbooks 72.4s 6 15 $0.2749
🚧 217_prometheus_compaction_heavy_queries — — — —
✅ 24_misconfigured_pvc 69.7s 7 17 $0.2751
✅ 43_current_datetime_from_prompt 4.9s 1 — $0.1070
✅ 61_exact_match_counting 21.0s 4 4 $0.1620
Total 78.9s avg 4.7 avg 10.1 avg $1.9430
📜 Run @ 2e625fd (#22060324393)

✅ Results of HolmesGPT evals

Automatically triggered by commit 2e625fd on branch claude/show-compaction-traces-fH5c2

View workflow logs

Results of HolmesGPT evals

  • ask_holmes: 1/12 test cases were successful, 0 regressions, 11 setup failures
Status Test case Time Turns Tools Cost
🚧 09_crashpod — — — —
🚧 101_loki_historical_logs_pod_deleted — — — —
✅ 111_pod_names_contain_service 39.1s 6 11 $0.2338
🚧 112_find_pvcs_by_uuid — — — —
🚧 12_job_crashing — — — —
🚧 161_conversation_compaction — — — —
🚧 163_compaction_follow_up — — — —
🚧 176_network_policy_blocking_traffic_no_runbooks — — — —
🚧 217_prometheus_compaction_heavy_queries — — — —
🚧 24_misconfigured_pvc — — — —
🚧 43_current_datetime_from_prompt — — — —
🚧 61_exact_match_counting — — — —
Total 39.1s avg 6.0 avg 11.0 avg $0.2338
📜 Run @ ced3966 (#22043627765)

✅ Results of HolmesGPT evals

Automatically triggered by commit ced3966 on branch claude/show-compaction-traces-fH5c2

View workflow logs

Results of HolmesGPT evals

  • ask_holmes: 9/9 test cases were successful, 0 regressions
Status Test case Time Turns Tools Cost
✅ 09_crashpod 30.8s 5 11 $0.2319
✅ 101_loki_historical_logs_pod_deleted 51.6s 7 14 $0.3072
✅ 111_pod_names_contain_service 31.6s 5 11 $0.2229
✅ 112_find_pvcs_by_uuid 38.5s 7 9 $0.2714
✅ 12_job_crashing 25.1s 4 8 $0.2096
✅ 176_network_policy_blocking_traffic_no_runbooks 46.6s 6 17 $0.2994
✅ 24_misconfigured_pvc 34.6s 6 14 $0.2400
✅ 43_current_datetime_from_prompt 4.5s 1 — $0.0111
✅ 61_exact_match_counting 16.4s 4 4 $0.1623
Total 31.1s avg 5.0 avg 11.0 avg $1.9558

✅ Results of HolmesGPT evals

Automatically triggered by commit 92e438a on branch claude/show-compaction-traces-fH5c2

View workflow logs

Results of HolmesGPT evals

  • ask_holmes: 1/12 test cases were successful, 0 regressions, 11 setup failures
Status Test case Time Turns Tools Cost Total tokens Input Output Cached Non-cached Reasoning Max output Compactions
🚧 09_crashpod — — — — — — — — — — — —
🚧 101_loki_historical_logs_pod_deleted — — — — — — — — — — — —
🚧 111_pod_names_contain_service — — — — — — — — — — — —
🚧 112_find_pvcs_by_uuid — — — — — — — — — — — —
🚧 12_job_crashing — — — — — — — — — — — —
🚧 161_conversation_compaction — — — — — — — — — — — —
🚧 163_compaction_follow_up — — — — — — — — — — — —
🚧 176_network_policy_blocking_traffic_no_runbooks — — — — — — — — — — — —
🚧 217_prometheus_compaction_heavy_queries — — — — — — — — — — — —
✅ 24_misconfigured_pvc 33.9s 5 14 $0.1505 106,541 104,532 2,009 95,757 8,775 — 587 —
🚧 43_current_datetime_from_prompt — — — — — — — — — — — —
🚧 61_exact_match_counting — — — — — — — — — — — —
Total 33.9s avg 5.0 avg 14.0 avg $0.1505 106,541 104,532 2,009 95,757 8,775 — 587 —
Benchmark Comparison Details

Baseline: latest ci-benchmark experiment on master

Status: Success - 61 test/model combinations loaded

Benchmark experiment:

Time comparison (seconds):

Test case This branch master Diff
09_crashpod (opus-4.5) — 29.5s —
101_loki_historical_logs_pod_deleted (opus-4.5) — — —
111_pod_names_contain_service (opus-4.5) — 34.2s —
112_find_pvcs_by_uuid (opus-4.5) — 31.0s —
12_job_crashing (opus-4.5) — 29.7s —
161_conversation_compaction (opus-4.5) — — —
163_compaction_follow_up (opus-4.5) — — —
176_network_policy_blocking_traffic_no_runbooks (opus-4.5) — 42.7s —
217_prometheus_compaction_heavy_queries (opus-4.5) — — —
24_misconfigured_pvc (opus-4.5) 33.9s 38.7s ↓12%
43_current_datetime_from_prompt (opus-4.5) — — —
61_exact_match_counting (opus-4.5) — — —

Benchmark has no cost, total tokens, cached tokens data. Will appear after the next weekly benchmark run.

Comparison indicators:

  • ±0% — diff under 10% (within noise threshold)
  • ↑N%/↓N% — diff 10-25%
  • ↑N%/↓N% — diff over 25% (significant)
📖 Legend
Icon Meaning
✅ The test was successful
➖ The test was skipped
⚠️ The test failed but is known to be flaky or known to fail
🚧 The test had a setup failure (not a code regression)
🔧 The test failed due to mock data issues (not a code regression)
🚫 The test was throttled by API rate limits/overload
❌ The test failed and should be fixed before merging the PR
🔄 Re-run evals manually

⚠️ Warning: /eval comments always run using the workflow from master, not from this PR branch. If you modified the GitHub Action (e.g., added secrets or env vars), those changes won't take effect.

To test workflow changes, use the GitHub CLI or Actions UI instead:

gh workflow run eval-regression.yaml --repo HolmesGPT/holmesgpt --ref claude/show-compaction-traces-fH5c2 -f markers=regression -f filter=

Option 1: Comment on this PR with /eval:

/eval
tags: regression

Or with more options (one per line):

/eval
model: gpt-4o
tags: regression
filter: 09_crashpod
iterations: 5

Run evals on a different branch (e.g., master) for comparison:

/eval
branch: master
tags: regression
Option Description
model Model(s) to test (default: same as automatic runs)
tags Pytest tags / markers (no default - runs all tests!)
filter Pytest -k filter (use /list to see valid eval names)
iterations Number of runs, max 10
branch Run evals on a different branch (for cross-branch comparison)

Quick re-run: Use /rerun to re-run the most recent /eval on this PR with the same parameters.

Option 2: Trigger via GitHub Actions UI → "Run workflow"

Option 3: Add PR labels to include extra evals in automatic regression runs:

Label Effect
evals-tag-<name> Run tests with tag <name> alongside regression
evals-id-<name> Run a specific eval by test ID

Examples: evals-tag-easy, evals-id-09_crashpod

🏷️ Valid tags

benchmark, chain-of-causation, compaction, confluence, context_window, coralogix, counting, database, datadog, datetime, db-connectors, easy, elasticsearch, embeds, fast, frontend, grafana-dashboard, hard, integration, kafka, kubernetes, leaked-information, logs, loki, medium, metrics, network, newrelic, no-cicd, numerical, one-test, port-forward, prometheus, question-answer, regression, runbooks, slackbot, storage, toolset-limitation, traces, transparency


Commands: /eval · /rerun · /list

CLI: gh workflow run eval-regression.yaml --repo HolmesGPT/holmesgpt --ref claude/show-compaction-traces-fH5c2 -f markers=regression -f filter=

@github-actions

github-actions Bot commented Feb 15, 2026 •

Copy link
Copy Markdown
Contributor

✅ Docker images ready for 0084ca7f (built in 5m 12s)

⚠️ Warning: does not support ARM (ARM images are built on release only - not on every PR)

Use these tags to pull the images for testing.

📋 Copy commands

⚠️ Temporary images are deleted after 30 days. Copy to a permanent registry before using them:

gcloud auth configure-docker us-central1-docker.pkg.dev
docker pull us-central1-docker.pkg.dev/robusta-development/temporary-builds/holmes:0084ca7f
docker tag us-central1-docker.pkg.dev/robusta-development/temporary-builds/holmes:0084ca7f me-west1-docker.pkg.dev/robusta-development/development/holmes-dev:0084ca7f
docker push me-west1-docker.pkg.dev/robusta-development/development/holmes-dev:0084ca7f
docker pull us-central1-docker.pkg.dev/robusta-development/temporary-builds/holmes-operator:0084ca7f
docker tag us-central1-docker.pkg.dev/robusta-development/temporary-builds/holmes-operator:0084ca7f me-west1-docker.pkg.dev/robusta-development/development/holmes-operator-dev:0084ca7f
docker push me-west1-docker.pkg.dev/robusta-development/development/holmes-operator-dev:0084ca7f

Patch Helm values in one line (choose the chart you use):

HolmesGPT chart:

helm upgrade --install holmesgpt ./helm/holmes \
  --set registry=me-west1-docker.pkg.dev/robusta-development/development \
  --set image=holmes-dev:0084ca7f \
  --set operator.registry=me-west1-docker.pkg.dev/robusta-development/development \
  --set operator.image=holmes-operator-dev:0084ca7f

Robusta wrapper chart:

helm upgrade --install robusta robusta/robusta \
  --reuse-values \
  --set holmes.registry=me-west1-docker.pkg.dev/robusta-development/development \
  --set holmes.image=holmes-dev:0084ca7f \
  --set holmes.operator.registry=me-west1-docker.pkg.dev/robusta-development/development \
  --set holmes.operator.image=holmes-operator-dev:0084ca7f

@coderabbitai

coderabbitai Bot commented Feb 15, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

This PR adds distributed tracing support to the conversation history compaction pipeline by propagating trace_span parameters through tool calling, context window limiter, and compaction functions. It includes new test fixtures and validation logic to verify compaction occurrence during test execution.

Changes

Cohort / File(s) Summary
Core Tracing Infrastructure
holmes/core/tool_calling_llm.py, holmes/core/truncation/compaction.py, holmes/core/truncation/input_context_window_limiter.py
Adds trace_span parameter propagation through compaction call chain; wraps LLM invocation and compaction logic in tracing spans with input/output logging, metrics recording, and event emission (CONVERSATION_HISTORY_COMPACTED, AI_MESSAGE).
Test Infrastructure Updates
tests/llm/test_ask_holmes.py, tests/llm/test_investigate.py, tests/llm/utils/test_case_utils.py
Adds assert_compaction field to HolmesTestCase, implements post-condition assertion for compaction metadata, and enhances exception context with test evaluation identifiers.
Compaction Test Updates
tests/llm/test_compaction.py
Propagates trace_span parameter to compact_conversation_history call.
Conversation Compaction Test Fixtures
tests/llm/fixtures/test_ask_holmes/161_conversation_compaction/...
Adds regression/compaction tags to test case, creates toolsets.yaml with kubernetes toolsets, and reduces resource requests in setup.sh for pods 8-10.
Compaction Follow-up Test Fixtures
tests/llm/fixtures/test_ask_holmes/163_compaction_follow_up/test_case.yaml
Adds regression tag to test case metadata.
Heavy Prometheus Compaction Test
tests/llm/fixtures/test_ask_holmes/217_prometheus_compaction_heavy_queries/manifest.yaml, test_case.yaml, toolsets.yaml
New end-to-end stress test with Kubernetes manifests (namespace, ConfigMap, 3 deployments generating CPU load), comprehensive test case with 30-minute Prometheus range queries, compaction assertion, and toolset configuration for kubernetes/prometheus integration.

Sequence Diagram

sequenceDiagram
    participant App as ToolCallingLLM
    participant Limiter as InputContextWindowLimiter
    participant Compaction as CompactionService
    participant TracingSpan as TracingSpan
    participant LLM as LLM

    App->>Limiter: call(llm, messages, trace_span)
    Limiter->>TracingSpan: start_span("Conversation History Compaction")
    TracingSpan->>Limiter: compaction_span
    Limiter->>Compaction: compact_conversation_history(history, llm, trace_span=compaction_span)
    Compaction->>TracingSpan: start_span(trace_span)
    TracingSpan->>LLM: completion(compaction_prompt)
    LLM-->>Compaction: response
    Compaction->>TracingSpan: log(success/failure)
    Compaction-->>Limiter: CompactionResult(compacted_history, compacted=true)
    Limiter->>TracingSpan: log_output(metrics)
    Limiter->>Limiter: emit(CONVERSATION_HISTORY_COMPACTED)
    Limiter->>Limiter: emit(AI_MESSAGE)
    Limiter-->>App: ContextWindowLimiterOutput(messages, metadata={conversation_history_compacted: true})
    App->>App: set metadata["conversation_history_compacted"] = True
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • moshemorad
  • arikalon1
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add distributed tracing support to conversation history compaction' directly and clearly summarizes the main change—adding tracing instrumentation to the compaction feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Feb 15, 2026 •

Copy link
Copy Markdown
Contributor

🔬 CLI Performance Benchmark

🟡 Startup Time (no LLM)

Measures holmes version execution time (imports + initialization)

Metric PR Master Change
Cold Start 10.60s 10.08s +5.2%
Warm Mean 4.71s 4.56s +3.4%
Warm Min 4.66s 4.53s
Warm Max 4.76s 4.59s

🟡 Full CLI with LLM

Measures holmes ask execution time (OpenRouter + Haiku 4.5)

Metric PR Master Change
Cold Start 19.11s 25.86s -26.1%
Warm Mean 7.73s 7.27s +6.3%
Warm Min 7.09s 6.69s
Warm Max 8.10s 7.73s

PR: b0d2f2c2 | Master: 2b640e32 | Iterations: 5

- Tag evals 161 and 163 with `regression` so they run in CI
- Create eval 217: Prometheus-heavy workload (35 pods) with a small
  context window (OVERRIDE_MAX_CONTEXT_SIZE=30000) that forces
  compaction to trigger naturally at the default 95% threshold
- Add `assert_compaction` field to HolmesTestCase so evals can fail
  if compaction did not occur
- Propagate `conversation_history_compacted` flag into LLMResult
  metadata so tests can inspect it

https://claude.ai/code/session_01KagxBSbTp1AbDQTvbk1KL8
Signed-off-by: Claude <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
tests/llm/fixtures/test_ask_holmes/217_prometheus_compaction_heavy_queries/manifest.yaml (2)

7-21: Unused ConfigMap cpu-script.

The cpu-script ConfigMap is defined but none of the three deployments mount or reference it. The payment-processors and order-handlers deployments use inline scripts instead. Remove it to avoid confusion.

🧹 Remove unused ConfigMap
 ---
-# ConfigMap with CPU load script
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  name: cpu-script
-  namespace: app-217
-data:
-  cpu-load.sh: |
-    #!/bin/sh
-    CPU_PERCENT=${CPU_PERCENT:-10}
-    BUSY_TIME=$(echo "scale=3; $CPU_PERCENT / 100" | bc)
-    IDLE_TIME=$(echo "scale=3; 1 - $BUSY_TIME" | bc)
-    while true; do
-      timeout ${BUSY_TIME}s sh -c 'while true; do echo "scale=5000; 4*a(1)" | bc -l > /dev/null; done' 2>/dev/null
-      sleep ${IDLE_TIME}
-    done
----
 # 20 low-CPU pods to generate many series in prometheus

72-81: apk add at container startup adds fragility and latency.

Both payment-processors and order-handlers install bc via apk add on every container start. If the Alpine package mirror is unavailable, all pods will fail. Consider using an image that already includes bc, or building a small custom image. For a test fixture this is acceptable, but it adds ~minutes of setup time across 15 pods and is a flaky failure point.

tests/llm/fixtures/test_ask_holmes/217_prometheus_compaction_heavy_queries/test_case.yaml (1)

41-41: Bare kubectl wait right after unrelated resource creation.

Line 41 waits on Prometheus pods that already exist (not newly created), so this isn't as risky as waiting on just-created resources. The || true fallback makes it non-fatal. Still, per coding guidelines, prefer a retry loop over bare kubectl wait for robustness.

Based on learnings: "Never use bare kubectl wait immediately after resource creation. Use retry loops to handle race conditions."

aantn and others added 3 commits February 16, 2026 16:08
- Test 161: add toolsets.yaml (kubernetes only) to avoid Loki
  prerequisite failure; reduce pod resource requests so all 10 pods
  can schedule on KIND cluster
- Test 217: replace kubectl run with kubectl exec for metric
  verification (avoids pod timeout); add retry loop
- Add [EVAL <id>] prefix to SetupFailureError messages
- Add test_id parameter to ToolsetPrerequisiteError
- Annotate all exceptions with eval ID via add_note() in both
  test_ask_holmes.py and test_investigate.py

https://claude.ai/code/session_01KagxBSbTp1AbDQTvbk1KL8
Signed-off-by: Claude <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/llm/fixtures/test_ask_holmes/161_conversation_compaction/setup.sh (1)

231-260: ⚠️ Potential issue | 🟡 Minor

CPU request values for Pods 8–9 now duplicate Pods 6–7, breaking the stated "lowest to highest" ordering.

Line 11 says the pods go "from lowest to highest" CPU, but after these changes the CPU requests are: 5, 25, 50, 75, 100, 150, 200, 150, 200, 250. Pods 8 and 9 duplicate the requests of Pods 6 and 7 respectively. The comments are also misleading — Pod 8 is labeled "Very high CPU (150m)" while Pod 6 is "High CPU (150m)" with the same value.

If the test only needs 10 pods generating load for conversation compaction, this may be acceptable. But if the scenario relies on a distinct, ascending CPU profile across all pods, consider using unique request values (e.g., 250m, 300m, 350m for Pods 8–10).

Also applies to: 262-291, 293-322

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/llm/fixtures/test_ask_holmes/161_conversation_compaction/setup.sh`
around lines 231 - 260, Pods 8–9 duplicate CPU request values of earlier pods,
breaking the intended ascending "lowest to highest" ordering; update the
resource requests for the pods defined by metadata.name search-indexer (and the
other two similar pod manifests referenced later) so all ten pods have unique,
strictly increasing cpu requests (for example change Pod 8 from 150m to 250m and
Pod 9 from 200m to 300m or pick another ascending sequence like 250m/300m/350m),
and update the inline comment labels to match the new cpu values so the comments
accurately describe each pod's CPU level.
🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.


In `@tests/llm/fixtures/test_ask_holmes/161_conversation_compaction/setup.sh`:
- Around line 231-260: Pods 8–9 duplicate CPU request values of earlier pods,
breaking the intended ascending "lowest to highest" ordering; update the
resource requests for the pods defined by metadata.name search-indexer (and the
other two similar pod manifests referenced later) so all ten pods have unique,
strictly increasing cpu requests (for example change Pod 8 from 150m to 250m and
Pod 9 from 200m to 300m or pick another ascending sequence like 250m/300m/350m),
and update the inline comment labels to match the new cpu values so the comments
accurately describe each pod's CPU level.

In
`@tests/llm/fixtures/test_ask_holmes/217_prometheus_compaction_heavy_queries/test_case.yaml`:
- Around line 36-91: The test uses a fixed "sleep 180" and two retry loops (the
pod readiness loop in the block that checks RUNNING_COUNT and the metrics
verification loop that checks METRICS_OUTPUT) which together can exceed the
declared setup_timeout (setup_timeout: 480); update either the setup_timeout to
~600 or reduce the 180s sleep and/or shorten loop retries so total worst‑case
time stays below setup_timeout, and replace the "kubectl wait
--for=condition=Ready pod -l app.kubernetes.io/name=prometheus -n default
--timeout=60s || true" with a non‑silent check (log a warning or fail
explicitly) so Prometheus readiness problems are surfaced before the metrics
checks run.

In `@tests/llm/utils/mock_toolset.py`:
- Around line 75-84: ToolsetPrerequisiteError's __init__ accepts test_id but no
callers pass it, so remove the dead parameter: change the constructor signature
in class ToolsetPrerequisiteError from def __init__(self, toolset_name: str,
error_detail: str, test_id: str = "") to def __init__(self, toolset_name: str,
error_detail: str), remove any logic that builds the prefix using test_id, and
construct the message using only toolset_name and error_detail before calling
super().__init__(message); keep all existing instantiations (they already omit
test_id) unchanged.

Comment on lines +36 to +91
before_test: |
# Verify Prometheus is available in the cluster
[ "$(kubectl get svc robusta-kube-prometheus-st-prometheus -n default -o jsonpath='{.spec.ports[?(@.port==9090)].port}' 2>/dev/null)" = "9090" ] \
|| { echo "❌ Prometheus Service missing or port 9090 not found"; exit 1; }

kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=prometheus -n default --timeout=60s || true

# Deploy workloads
kubectl apply -f ./manifest.yaml

# Wait for all pods to be running
POD_READY=false
for attempt in {1..120}; do
RUNNING_COUNT=$(kubectl get pods -n app-217 --field-selector=status.phase=Running --no-headers 2>/dev/null | wc -l)
echo "⏳ Attempt $attempt/120: $RUNNING_COUNT/35 pods running..."
if [ "$RUNNING_COUNT" -ge 35 ]; then
echo "✅ All pods are running!"
POD_READY=true
break
fi
sleep 2
done

if [ "$POD_READY" = false ]; then
echo "❌ Not all pods became ready"
kubectl get pods -n app-217
exit 1
fi

# Wait for CPU metrics to accumulate
echo "Waiting 3 minutes for metrics to accumulate..."
sleep 180

# Verify metrics are available using kubectl exec on an existing pod
WORKER_POD=$(kubectl get pods -n app-217 -l app=api-worker -o jsonpath='{.items[0].metadata.name}')
METRICS_CHECK=false
for i in {1..10}; do
METRICS_OUTPUT=$(kubectl exec -n app-217 "$WORKER_POD" -- \
wget -q -O- "http://robusta-kube-prometheus-st-prometheus.default.svc.cluster.local:9090/api/v1/query?query=count(rate(container_cpu_usage_seconds_total\{namespace=%22app-217%22\}[2m]))" 2>&1) || true

if echo "$METRICS_OUTPUT" | grep -q '"status":"success"'; then
if ! echo "$METRICS_OUTPUT" | grep -q '"result":\[\]'; then
echo "✅ CPU metrics available for app-217"
METRICS_CHECK=true
break
fi
fi
echo "⏳ Metrics check attempt $i/10..."
sleep 10
done

if [ "$METRICS_CHECK" = false ]; then
echo "❌ No CPU metrics found for app-217 after retries"
echo "Last response: $METRICS_OUTPUT"
exit 1
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

setup_timeout: 480 with sleep 180 — the fixed 3-minute sleep dominates setup time.

The setup includes a hard 3-minute sleep for metrics accumulation. Combined with the pod readiness loop (up to 240s) and metrics verification loop (up to 100s), the total could approach 520s — exceeding the 480s setup_timeout. Consider either increasing setup_timeout to ~600 or reducing the sleep if feasible.

Also, line 41's kubectl wait ... || true silently continues if Prometheus isn't ready, which could lead to confusing failures during the metrics verification step. Consider logging a warning instead of silently ignoring.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/llm/fixtures/test_ask_holmes/217_prometheus_compaction_heavy_queries/test_case.yaml`
around lines 36 - 91, The test uses a fixed "sleep 180" and two retry loops (the
pod readiness loop in the block that checks RUNNING_COUNT and the metrics
verification loop that checks METRICS_OUTPUT) which together can exceed the
declared setup_timeout (setup_timeout: 480); update either the setup_timeout to
~600 or reduce the 180s sleep and/or shorten loop retries so total worst‑case
time stays below setup_timeout, and replace the "kubectl wait
--for=condition=Ready pod -l app.kubernetes.io/name=prometheus -n default
--timeout=60s || true" with a non‑silent check (log a warning or fail
explicitly) so Prometheus readiness problems are surfaced before the metrics
checks run.

Comment thread tests/llm/utils/mock_toolset.py Outdated
Comment on lines 75 to 84
def __init__(self, toolset_name: str, error_detail: str, test_id: str = ""):
self.toolset_name = toolset_name
self.error_detail = error_detail
self.test_id = test_id
prefix = f"[EVAL {test_id}] " if test_id else ""
message = (
f"Toolset '{toolset_name}' was explicitly enabled in toolsets.yaml "
f"{prefix}Toolset '{toolset_name}' was explicitly enabled in toolsets.yaml "
f"but failed prerequisites check: {error_detail}"
)
super().__init__(message)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n 'ToolsetPrerequisiteError' --type=py -C2

Repository: HolmesGPT/holmesgpt

Length of output: 4421


🏁 Script executed:

rg 'ToolsetPrerequisiteError\s*\(' --type=py -A2 | grep -v "^tests/llm/utils/mock_toolset.py"

Repository: HolmesGPT/holmesgpt

Length of output: 397


The test_id parameter is accepted but never passed by any caller.

All ToolsetPrerequisiteError(...) instantiations across the codebase (only in this file at lines 832, 842, 871, 878, 901, 908) omit test_id, so the message prefix will always be empty. The parameter has a default value, ensuring backward compatibility, but it is dead code. Consider removing it unless it is required for future use or external integration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/llm/utils/mock_toolset.py` around lines 75 - 84,
ToolsetPrerequisiteError's __init__ accepts test_id but no callers pass it, so
remove the dead parameter: change the constructor signature in class
ToolsetPrerequisiteError from def __init__(self, toolset_name: str,
error_detail: str, test_id: str = "") to def __init__(self, toolset_name: str,
error_detail: str), remove any logic that builds the prefix using test_id, and
construct the message using only toolset_name and error_detail before calling
super().__init__(message); keep all existing instantiations (they already omit
test_id) unchanged.

aantn and others added 3 commits February 20, 2026 16:26
…ion-traces-fH5c2

Signed-off-by: Claude <noreply@anthropic.com>

# Conflicts:
#	holmes/core/truncation/compaction.py
#	holmes/core/truncation/input_context_window_limiter.py
#	tests/llm/test_compaction.py
#	tests/llm/utils/mock_toolset.py
#	tests/llm/utils/test_case_utils.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
holmes/core/tool_calling_llm.py (1)

1009-1016: Consider adding trace_span parameter to call_stream() for consistency.

Unlike call(), call_stream() does not accept or propagate a trace_span parameter to limit_input_context_window. The TODO at line 1150 acknowledges streaming doesn't support tracing yet. Consider creating a follow-up issue to add tracing support to the streaming path for full observability.

Would you like me to open an issue to track adding tracing support to call_stream()?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/core/tool_calling_llm.py` around lines 1009 - 1016, call_stream
currently doesn't accept or forward the trace_span used by call, so tracing
isn't propagated into limit_input_context_window; update the call_stream
signature to accept an optional trace_span parameter (matching call), pass that
trace_span into the limit_input_context_window call inside call_stream, and
ensure any downstream propagation (e.g., when yielding events or composing
metadata) preserves/merges the trace_span info; also add a short TODO or open an
issue to cover streaming-specific tracing details if further instrumentation is
needed.
holmes/core/truncation/input_context_window_limiter.py (1)

152-157: Add type annotation and consider module-level singleton for trace_span default.

The trace_span parameter lacks a type hint, which is required per coding guidelines. Additionally, Ruff B008 flags the DummySpan() call in the default argument—while DummySpan is stateless and safe here, using a module-level singleton is more idiomatic.

♻️ Proposed fix

Add a module-level singleton and type annotation:

 from holmes.core.tracing import DummySpan
+from holmes.core.tracing import DummySpan as DummySpanType  # For typing
 from holmes.core.truncation.compaction import CompactionUsage, compact_conversation_history
+
+_DUMMY_SPAN = DummySpan()

Then update the signature:

 `@sentry_sdk.trace`
 def limit_input_context_window(
     llm: LLM,
     messages: list[dict],
     tools: Optional[list[dict[str, Any]]],
-    trace_span=DummySpan(),
+    trace_span: "DummySpanType" = _DUMMY_SPAN,
 ) -> ContextWindowLimiterOutput:

Alternatively, if a proper Span protocol/base class exists in tracing.py, use that for the type annotation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/core/truncation/input_context_window_limiter.py` around lines 152 -
157, The function limit_input_context_window has an untyped parameter trace_span
and uses DummySpan() as a mutable default; add an explicit type annotation for
trace_span (preferably the Span protocol/class from tracing.py if available) and
replace the inline DummySpan() default with a module-level singleton (e.g.,
MODULE_DUMMY_SPAN = DummySpan()) then update the signature to use that singleton
as the default (or use Optional[Span] and set the default to None and assign the
module singleton inside the function); reference limit_input_context_window,
trace_span, and DummySpan when making the change.
holmes/core/truncation/compaction.py (1)

62-64: Add type annotation and consider module-level singleton for trace_span default.

Same issue as in input_context_window_limiter.py: the trace_span parameter lacks a type hint and triggers Ruff B008.

♻️ Proposed fix
 from holmes.core.tracing import DummySpan
+
+_DUMMY_SPAN = DummySpan()
 def compact_conversation_history(
-    original_conversation_history: list[dict], llm: LLM, trace_span=DummySpan()
+    original_conversation_history: list[dict], llm: LLM, trace_span=_DUMMY_SPAN
 ) -> CompactionResult:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/core/truncation/compaction.py` around lines 62 - 64, The parameter
trace_span on compact_conversation_history lacks a type annotation and uses a
fresh DummySpan() as a default which triggers Ruff B008; add an explicit type
hint (e.g., trace_span: Span or whatever tracing interface type is used across
the codebase) and replace the inline DummySpan() default with a module-level
singleton (e.g., SINGLETON_TRACE_SPAN = DummySpan()) or use trace_span:
Optional[Span] = None and then inside compact_conversation_history set
trace_span = trace_span or SINGLETON_TRACE_SPAN to avoid creating a new object
at import time and satisfy the linter; reference compact_conversation_history
and DummySpan when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@holmes/core/truncation/compaction.py`:
- Around line 85-90: The span status logging in the Compaction LLM Call block is
too lax (it logs "success" when response and response.choices are truthy) but
the actual success path checks response.choices[0] and
response.choices[0].message; update the llm_span.log call inside
trace_span.start_span (the block around trace_span.start_span/name "Compaction
LLM Call") so the output status uses the same condition as the later success
check (i.e., ensure response is truthy, response.choices has elements,
response.choices[0] exists, and response.choices[0].message is present) and log
"success" only when that full condition is met, otherwise "failure".

---

Nitpick comments:
In `@holmes/core/tool_calling_llm.py`:
- Around line 1009-1016: call_stream currently doesn't accept or forward the
trace_span used by call, so tracing isn't propagated into
limit_input_context_window; update the call_stream signature to accept an
optional trace_span parameter (matching call), pass that trace_span into the
limit_input_context_window call inside call_stream, and ensure any downstream
propagation (e.g., when yielding events or composing metadata) preserves/merges
the trace_span info; also add a short TODO or open an issue to cover
streaming-specific tracing details if further instrumentation is needed.

In `@holmes/core/truncation/compaction.py`:
- Around line 62-64: The parameter trace_span on compact_conversation_history
lacks a type annotation and uses a fresh DummySpan() as a default which triggers
Ruff B008; add an explicit type hint (e.g., trace_span: Span or whatever tracing
interface type is used across the codebase) and replace the inline DummySpan()
default with a module-level singleton (e.g., SINGLETON_TRACE_SPAN = DummySpan())
or use trace_span: Optional[Span] = None and then inside
compact_conversation_history set trace_span = trace_span or SINGLETON_TRACE_SPAN
to avoid creating a new object at import time and satisfy the linter; reference
compact_conversation_history and DummySpan when making the change.

In `@holmes/core/truncation/input_context_window_limiter.py`:
- Around line 152-157: The function limit_input_context_window has an untyped
parameter trace_span and uses DummySpan() as a mutable default; add an explicit
type annotation for trace_span (preferably the Span protocol/class from
tracing.py if available) and replace the inline DummySpan() default with a
module-level singleton (e.g., MODULE_DUMMY_SPAN = DummySpan()) then update the
signature to use that singleton as the default (or use Optional[Span] and set
the default to None and assign the module singleton inside the function);
reference limit_input_context_window, trace_span, and DummySpan when making the
change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bf8b36ca-b13e-4b32-81c4-d029e40c9e98

📥 Commits

Reviewing files that changed from the base of the PR and between 974433e and 92e438a.

📒 Files selected for processing (3)
  • holmes/core/tool_calling_llm.py
  • holmes/core/truncation/compaction.py
  • holmes/core/truncation/input_context_window_limiter.py

Comment on lines +85 to +90
with trace_span.start_span(name="Compaction LLM Call", type="llm") as llm_span:
llm_span.log(input={"message_count": len(conversation_history)})
response: ModelResponse = llm.completion(
messages=conversation_history, drop_params=True
) # type: ignore
llm_span.log(output={"status": "success" if response and response.choices else "failure"})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Status logging may be inconsistent with actual outcome.

The span logs "success" when response and response.choices is truthy, but the actual success check (lines 96-102) is more stringent—it also verifies response.choices[0] and response.choices[0].message. If the response has choices but the first choice lacks a message, the span logs "success" while the function returns the original history (a failure).

Consider aligning the status check:

🐛 Proposed fix
             response: ModelResponse = llm.completion(
                 messages=conversation_history, drop_params=True
             )  # type: ignore
-            llm_span.log(output={"status": "success" if response and response.choices else "failure"})
+            has_valid_response = (
+                response
+                and response.choices
+                and response.choices[0]
+                and response.choices[0].message
+            )
+            llm_span.log(output={"status": "success" if has_valid_response else "failure"})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
with trace_span.start_span(name="Compaction LLM Call", type="llm") as llm_span:
llm_span.log(input={"message_count": len(conversation_history)})
response: ModelResponse = llm.completion(
messages=conversation_history, drop_params=True
) # type: ignore
llm_span.log(output={"status": "success" if response and response.choices else "failure"})
with trace_span.start_span(name="Compaction LLM Call", type="llm") as llm_span:
llm_span.log(input={"message_count": len(conversation_history)})
response: ModelResponse = llm.completion(
messages=conversation_history, drop_params=True
) # type: ignore
has_valid_response = (
response
and response.choices
and response.choices[0]
and response.choices[0].message
)
llm_span.log(output={"status": "success" if has_valid_response else "failure"})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@holmes/core/truncation/compaction.py` around lines 85 - 90, The span status
logging in the Compaction LLM Call block is too lax (it logs "success" when
response and response.choices are truthy) but the actual success path checks
response.choices[0] and response.choices[0].message; update the llm_span.log
call inside trace_span.start_span (the block around trace_span.start_span/name
"Compaction LLM Call") so the output status uses the same condition as the later
success check (i.e., ensure response is truthy, response.choices has elements,
response.choices[0] exists, and response.choices[0].message is present) and log
"success" only when that full condition is met, otherwise "failure".

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants