Skip to content
Back to skills

525 2026 02 02 Sanitize Loop Canonicalization Bottleneck 60c282ab

ASecurity

**Date:** 2026-02-02 **Phase:** Research **Branch:** bottlenecks

  • 9 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added June 1, 2026
ai-agentspython

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned June 1, 2026

npx -y skills add tools-only/X-Skills --skill 525-2026-02-02_sanitize_loop_canonicalization_bottleneck_60c282ab --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 525 2026 02 02 Sanitize Loop Canonicalization Bottleneck 60c282ab?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 525 2026 02 02 Sanitize Loop Canonicalization Bottleneck 60c282ab
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-525-2026-02-02-sanitize-loop-canonicalization-bott/badge)](https://www.skillsdirectory.com/skills/tools-only-525-2026-02-02-sanitize-loop-canonicalization-bott)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
# Research: Sanitize Loop Canonicalization Bottleneck

**Date:** 2026-02-02
**Phase:** Research
**Branch:** bottlenecks

## Problem Statement

`run_cleanup_loop()` performs 4N message conversions per iteration (up to 10 iterations), resulting in worst-case 40N conversions for N messages.

## Structure

### File Layout
```
src/tunacode/
├── core/agents/resume/sanitize.py     # Cleanup loop orchestrator
├── utils/messaging/adapter.py          # Message conversion + tool call extraction
└── types/canonical.py                   # CanonicalMessage definition
```

### Entry Point
- `sanitize.py:458-502` - `run_cleanup_loop(messages, tool_registry)`
- Called from: `main.py:291`

## Key Files

### sanitize.py - Cleanup Loop

| Location | Function | Purpose |
|----------|----------|---------|
| `sanitize.py:38` | `MAX_CLEANUP_ITERATIONS = 10` | Loop iteration cap |
| `sanitize.py:61-63` | `_canonicalize_messages()` | Delegates to `to_canonical_list()` |
| `sanitize.py:248-250` | `find_dangling_tool_call_ids()` | Delegates to adapter |
| `sanitize.py:326-355` | `remove_empty_responses()` | Converts all messages at line 332 |
| `sanitize.py:358-404` | `remove_consecutive_requests()` | Converts all messages at line 364 |
| `sanitize.py:458-502` | `run_cleanup_loop()` | Main loop - 3 cleanup steps per iteration |

### adapter.py - Conversion Functions

| Location | Function | Conversions |
|----------|----------|-------------|
| `adapter.py:146` | `to_canonical(message)` | 1 per call |
| `adapter.py:205-207` | `to_canonical_list(messages)` | N per call |
| `adapter.py:295-299` | `get_tool_call_ids(msg)` | 1 per call (if not already canonical) |
| `adapter.py:302-306` | `get_tool_return_ids(msg)` | 1 per call (if not already canonical) |
| `adapter.py:309-318` | `find_dangling_tool_calls(messages)` | 2N per call (see bottleneck 1) |

## Patterns Found

### Pattern 1: Double Conversion in find_dangling_tool_calls

**Location:** `adapter.py:309-318`

```python
def find_dangling_tool_calls(messages: list[Any]) -> set[str]:
    call_ids: set[str] = set()
    return_ids: set[str] = set()

    for msg in messages:                         # N iterations
        call_ids.update(get_tool_call_ids(msg))   # → to_canonical(msg)  [conversion 1]
        return_ids.update(get_tool_return_ids(msg))  # → to_canonical(msg)  [conversion 2]

    return call_ids - return_ids
```

**Observation:** Same message converted twice per loop iteration. The canonical result from `get_tool_call_ids()` is discarded before `get_tool_return_ids()` re-converts the same message.

### Pattern 2: Conversion-for-Inspection in remove_empty_responses

**Location:** `sanitize.py:326-355`

```python
def remove_empty_responses(messages: list[Any]) -> bool:
    canonical_messages = _canonicalize_messages(messages)  # Line 332: N conversions
    # ... inspection logic ...
    del messages[index]  # Line 351: mutates ORIGINAL list, not canonical
```

**Observation:** Canonical messages used only for read-only inspection. Mutations happen on original pydantic-ai list.

### Pattern 3: Identical Conversion-for-Inspection in remove_consecutive_requests

**Location:** `sanitize.py:358-404`

```python
def remove_consecutive_requests(messages: list[Any]) -> bool:
    canonical_messages = _canonicalize_messages(messages)  # Line 364: N conversions
    # ... inspection logic ...
    del messages[index]  # Line 400: mutates ORIGINAL list
```

**Observation:** Same pattern as Pattern 2. Independent conversion with no shared cache.

### Pattern 4: No Caching Between Functions

**Location:** `sanitize.py:469-491`

```python
for cleanup_iteration in range(MAX_CLEANUP_ITERATIONS):
    dangling_tool_call_ids = find_dangling_tool_call_ids(messages)  # 2N conversions
    # ...
    removed_empty_responses = remove_empty_responses(messages)       # N conversions
    # ...
    removed_consecutive_requests = remove_consecutive_requests(messages)  # N conversions
```

**Observation:** Three functions called sequentially, each converts the entire message list independently. No shared canonical cache within an iteration.

## Dependencies

### Conversion Call Chain

```
run_cleanup_loop (sanitize.py:458)
├── find_dangling_tool_call_ids (sanitize.py:248)
│   └── find_dangling_tool_calls (adapter.py:309)
│       ├── get_tool_call_ids (adapter.py:295)
│       │   └── to_canonical (adapter.py:146)  # 1 per msg
│       └── get_tool_return_ids (adapter.py:302)
│           └── to_canonical (adapter.py:146)  # 1 per msg (DUPLICATE)
├── remove_empty_responses (sanitize.py:326)
│   └── _canonicalize_messages (sanitize.py:61)
│       └── to_canonical_list (adapter.py:205)  # N conversions
└── remove_consecutive_requests (sanitize.py:358)
    └── _canonicalize_messages (sanitize.py:61)
        └── to_canonical_list (adapter.py:205)  # N conversions
```

### Import Dependencies

```
sanitize.py imports from adapter.py:
├── find_dangling_tool_calls
└── to_canonical_list (via _canonicalize_messages)

sanitize.py imports from canonical.py:
├── CanonicalMessage
├── MessageRole
└── SystemPromptPart
```

## Conversion Count Summary

### Per Iteration (N messages)

| Function | Location | Conversions |
|----------|----------|-------------|
| `find_dangling_tool_call_ids()` via `get_tool_call_ids()` | `adapter.py:315` | N |
| `find_dangling_tool_call_ids()` via `get_tool_return_ids()` | `adapter.py:316` | N |
| `remove_empty_responses()` | `sanitize.py:332` | N |
| `remove_consecutive_requests()` | `sanitize.py:364` | N |
| **Total per iteration** | | **4N** |

### Total Conversions

| Scenario | Messages | Iterations | Total Conversions |
|----------|----------|------------|-------------------|
| Best case | 100 | 1 | 400 |
| Typical | 100 | 3 | 1,200 |
| Worst case | 100 | 10 | 4,000 |
| Worst case | 500 | 10 | 20,000 |
| Worst case | 1000 | 10 | 40,000 |

## Symbol Index

### sanitize.py Exports

- `sanitize_history_for_resume()` - Main entry point (line 412)
- `run_cleanup_loop()` - Loop orchestrator (line 458)
- `find_dangling_tool_call_ids()` - ID extraction (line 248)
- `remove_dangling_tool_calls()` - Removal (line 253)
- `remove_empty_responses()` - Cleanup (line 326)
- `remove_consecutive_requests()` - Cleanup (line 358)

### adapter.py Exports (relevant)

- `to_canonical()` - Single message conversion (line 146)
- `to_canonical_list()` - Batch conversion (line 205)
- `get_tool_call_ids()` - ID extraction (line 295)
- `get_tool_return_ids()` - ID extraction (line 302)
- `find_dangling_tool_calls()` - Detection (line 309)

## Bottleneck Root Causes

1. **Double conversion in find_dangling_tool_calls**: Same message converted twice to extract `call_ids` then `return_ids`

2. **Independent conversions per function**: Each cleanup function (`find_dangling`, `remove_empty`, `remove_consecutive`) converts the full list independently

3. **No per-iteration cache**: Canonical messages created and discarded by each function; no shared cache within an iteration

4. **Read-only conversions on mutable data**: Conversions used only for inspection while mutations happen on original pydantic-ai list

## Fix Locations

| Fix | File | Lines | Impact |
|-----|------|-------|--------|
| Single conversion in find_dangling | `adapter.py` | 309-318 | -N per iteration |
| Cache canonical at iteration start | `sanitize.py` | 469-491 | -2N per iteration |
| Pass cache to cleanup functions | `sanitize.py` | 326, 364 | Eliminates redundant calls |

**After fixes:** 1N conversions per iteration (down from 4N) = 75% reduction

Files in this skill

  • README.md1 KB
  • skill.md7.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.