Skip to content

Commit e80f11f

Browse files
committed
fix: reduce loop detection false positives
1 parent bc3be27 commit e80f11f

10 files changed

Lines changed: 98 additions & 26 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"name": "token-optimizer",
1414
"source": "./",
1515
"description": "Audit, fix, and monitor Claude Code context window usage. Find the ghost tokens.",
16-
"version": "5.6.3",
16+
"version": "5.6.4",
1717
"author": {
1818
"name": "Alex Greenshpun",
1919
"url": "https://linkedin.com/in/alexgreensh"

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
},
88
"homepage": "https://github.com/alexgreensh/token-optimizer",
99
"repository": "https://github.com/alexgreensh/token-optimizer",
10-
"version": "5.6.3",
10+
"version": "5.6.4",
1111
"license": "PolyForm-Noncommercial-1.0.0",
1212
"keywords": ["token", "optimization", "context", "audit", "cost", "coach"]
1313
}

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
</p>
44

55
<p align="center">
6-
<a href="https://github.com/alexgreensh/token-optimizer/releases"><img src="https://img.shields.io/badge/version-5.6.2-green" alt="Version 5.6.2"></a>
6+
<a href="https://github.com/alexgreensh/token-optimizer/releases"><img src="https://img.shields.io/badge/version-5.6.4-green" alt="Version 5.6.4"></a>
77
<a href="https://github.com/alexgreensh/token-optimizer"><img src="https://img.shields.io/badge/Claude_Code-Plugin-blueviolet" alt="Claude Code Plugin"></a>
8-
<a href="https://github.com/alexgreensh/token-optimizer/tree/main/openclaw"><img src="https://img.shields.io/badge/OpenClaw-v2.4.0-brightgreen" alt="OpenClaw v2.4.0"></a>
8+
<a href="https://github.com/alexgreensh/token-optimizer/tree/main/openclaw"><img src="https://img.shields.io/badge/OpenClaw-v2.4.1-brightgreen" alt="OpenClaw v2.4.1"></a>
99
<a href="https://github.com/alexgreensh/token-optimizer/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-PolyForm%20Noncommercial-blue.svg" alt="License: PolyForm Noncommercial"></a>
1010
<a href="https://github.com/alexgreensh/token-optimizer/stargazers"><img src="https://img.shields.io/github/stars/alexgreensh/token-optimizer" alt="GitHub Stars"></a>
1111
<a href="https://github.com/alexgreensh/token-optimizer/commits/main"><img src="https://img.shields.io/github/last-commit/alexgreensh/token-optimizer" alt="Last Commit"></a>

openclaw/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Token Optimizer for OpenClaw
22

3-
Version: `2.3.1`
3+
Version: `2.4.1`
44

55
**Your AI is getting dumber and you can't see it.**
66

openclaw/dist/cli.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

openclaw/openclaw.plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"id": "token-optimizer-openclaw",
33
"name": "Token Optimizer",
44
"description": "Find the ghost tokens. Audit your OpenClaw setup, see where context goes, fix it.",
5-
"version": "2.4.0",
5+
"version": "2.4.1",
66
"skills": ["skills/token-optimizer"],
77
"configSchema": {
88
"type": "object",

openclaw/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

openclaw/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "token-optimizer-openclaw",
3-
"version": "2.4.0",
3+
"version": "2.4.1",
44
"description": "Token waste auditor for OpenClaw. Detects idle burns, model misrouting, and context bloat with dollar savings.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

openclaw/src/cli.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function redactPaths(obj: unknown): unknown {
4242
}
4343

4444
function printUsage(): void {
45-
console.log(`Token Optimizer for OpenClaw v2.3.1
45+
console.log(`Token Optimizer for OpenClaw v2.4.1
4646
4747
Usage:
4848
token-optimizer scan [--days N] [--json] Scan sessions and show token usage
@@ -142,7 +142,7 @@ function cmdV5Toggle(featureId: string, on: boolean): void {
142142

143143
function cmdV5Welcome(): void {
144144
const features = listV5Features();
145-
console.log(`\nWelcome to Token Optimizer v2.3.1!`);
145+
console.log(`\nWelcome to Token Optimizer v2.4.1!`);
146146
console.log("=".repeat(50));
147147
console.log(
148148
"v5 Active Compression is now live in OpenClaw. The low-risk features ship ON by default; the rest stay opt-in until you flip them on:\n"

skills/token-optimizer/scripts/measure.py

Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7955,7 +7955,7 @@ def setup_hook(dry_run=False):
79557955

79567956
# ========== Persistent Dashboard Daemon ==========
79577957

7958-
TOKEN_OPTIMIZER_VERSION = "5.6.3" # Keep in sync with plugin.json + marketplace.json
7958+
TOKEN_OPTIMIZER_VERSION = "5.6.4" # Keep in sync with plugin.json + marketplace.json
79597959
DAEMON_LABEL = "com.token-optimizer.dashboard"
79607960
DAEMON_PORT = 24842 # Memorable: 2-4-8-4-2 (powers of 2 palindrome), avoids common ports
79617961
LAUNCH_AGENTS_DIR = Path.home() / "Library" / "LaunchAgents"
@@ -10052,6 +10052,8 @@ def _parse_jsonl_for_quality(filepath):
1005210052
reads = [] # (index, path, timestamp)
1005310053
writes = [] # (index, path, timestamp)
1005410054
tool_results = [] # (index, tool_name, result_size_chars, referenced_later)
10055+
tool_result_meta = [] # richer metadata for live detectors
10056+
tool_name_by_id = {}
1005510057
system_reminders = [] # (index, content_hash, size_chars)
1005610058
messages = [] # (index, role, text_length, is_substantive)
1005710059
compactions = 0
@@ -10091,6 +10093,8 @@ def _parse_jsonl_for_quality(filepath):
1009110093
reads = []
1009210094
writes = []
1009310095
tool_results = []
10096+
tool_result_meta = []
10097+
tool_name_by_id = {}
1009410098
system_reminders = []
1009510099
messages = []
1009610100
agent_dispatches = []
@@ -10136,6 +10140,9 @@ def _parse_jsonl_for_quality(filepath):
1013610140
elif block.get("type") == "tool_use":
1013710141
is_substantive = True # tool invocations ARE decisions
1013810142
tool_name = block.get("name", "")
10143+
tool_id = block.get("id", "")
10144+
if tool_id:
10145+
tool_name_by_id[tool_id] = tool_name
1013910146
inp = block.get("input", {})
1014010147

1014110148
if tool_name == "Read":
@@ -10166,6 +10173,13 @@ def _parse_jsonl_for_quality(filepath):
1016610173
result_text = _extract_tool_result_text(block)
1016710174
tool_id = block.get("tool_use_id", "")
1016810175
tool_results.append((idx, tool_id, len(result_text), False))
10176+
tool_result_meta.append({
10177+
"index": idx,
10178+
"tool_id": tool_id,
10179+
"tool_name": tool_name_by_id.get(tool_id, ""),
10180+
"size": len(result_text),
10181+
"is_failure": _tool_result_looks_failed(block, result_text),
10182+
})
1016910183

1017010184
# Update agent dispatch result sizes
1017110185
if agent_dispatches and agent_dispatches[-1][2] == 0:
@@ -10184,6 +10198,7 @@ def _parse_jsonl_for_quality(filepath):
1018410198
"reads": reads,
1018510199
"writes": writes,
1018610200
"tool_results": tool_results,
10201+
"tool_result_meta": tool_result_meta,
1018710202
"system_reminders": system_reminders,
1018810203
"messages": messages,
1018910204
"compactions": compactions,
@@ -10746,6 +10761,55 @@ def _extract_tool_result_text(block):
1074610761
return str(rc)
1074710762

1074810763

10764+
_TOOL_FAILURE_RE = re.compile(
10765+
r"("
10766+
r"\btraceback\b|"
10767+
r"\bexception\b|"
10768+
r"\bfailed\b|"
10769+
r"\bfailure\b|"
10770+
r"\bfatal:|"
10771+
r"\berror:|"
10772+
r"\bpermission denied\b|"
10773+
r"\bno such file or directory\b|"
10774+
r"\bcommand not found\b|"
10775+
r"\bexit (?:code|status) [2-9]\d*\b|"
10776+
r"\bexited with code [2-9]\d*\b|"
10777+
r"\breturned non-zero\b|"
10778+
r"\breturncode [2-9]\d*\b|"
10779+
r"\btimed out\b|"
10780+
r"\bsyntaxerror\b|"
10781+
r"\btypeerror\b|"
10782+
r"\bvalueerror\b|"
10783+
r"\bassertionerror\b|"
10784+
r"\bnpm err!|"
10785+
r"\btests? failed\b"
10786+
r")",
10787+
re.IGNORECASE,
10788+
)
10789+
10790+
10791+
_TOOL_SUCCESS_COUNT_RE = re.compile(
10792+
r"\b(?:0 failed|0 failures|0 errors|no failures|no errors)\b",
10793+
re.IGNORECASE,
10794+
)
10795+
_TOOL_NONZERO_FAILURE_COUNT_RE = re.compile(
10796+
r"\b[1-9]\d*\s+(?:failed|failures|errors)\b",
10797+
re.IGNORECASE,
10798+
)
10799+
10800+
10801+
def _tool_result_looks_failed(block, result_text):
10802+
"""Return True only for result blocks that carry a concrete failure signal."""
10803+
if block.get("is_error") is True:
10804+
return True
10805+
text = (result_text or "").strip()
10806+
if not text:
10807+
return False
10808+
if _TOOL_SUCCESS_COUNT_RE.search(text) and not _TOOL_NONZERO_FAILURE_COUNT_RE.search(text):
10809+
return False
10810+
return bool(_TOOL_FAILURE_RE.search(text[:2000]))
10811+
10812+
1074910813
def _resolve_jsonl_path(arg=None):
1075010814
"""Resolve a JSONL file path from a session ID, file path, or auto-detect.
1075110815

@@ -14418,20 +14482,28 @@ def _check_realtime_loops(quality_data):
1441814482
})
1441914483

1442014484
# --- Retry churn detection ---
14421-
tool_results = quality_data.get("tool_results", [])
14422-
if len(tool_results) >= 3:
14423-
recent_tools = tool_results[-5:]
14424-
# tool_results entries are: (index, tool_id, result_size_chars, referenced_later)
14425-
# Check for repeated small results (errors tend to be short)
14426-
small_results = [t for t in recent_tools if t[2] < 200] # short results = likely errors
14427-
if len(small_results) >= 3:
14428-
# If 3+ of the last 5 tool results are very short, might be error loop
14429-
sizes = [t[2] for t in small_results]
14430-
if all(abs(s - sizes[0]) < 50 for s in sizes):
14485+
# Short tool results are common for successful operations ("done",
14486+
# empty search results, concise shell output). Only warn when recent
14487+
# short results also carry concrete failure signals and come from
14488+
# the same tool family.
14489+
tool_result_meta = quality_data.get("tool_result_meta", [])
14490+
if len(tool_result_meta) >= 3:
14491+
recent_tools = tool_result_meta[-5:]
14492+
short_failures = [
14493+
t for t in recent_tools
14494+
if t.get("is_failure") and t.get("size", 0) < 400
14495+
]
14496+
if len(short_failures) >= 3:
14497+
by_tool = {}
14498+
for item in short_failures:
14499+
tool_name = item.get("tool_name") or "unknown"
14500+
by_tool[tool_name] = by_tool.get(tool_name, 0) + 1
14501+
most_repeated = max(by_tool.values()) if by_tool else 0
14502+
if most_repeated >= 3:
1443114503
warnings.append({
1443214504
"type": "retry_churn",
14433-
"confidence": 0.6,
14434-
"count": len(small_results),
14505+
"confidence": 0.75,
14506+
"count": most_repeated,
1443514507
})
1443614508

1443714509
except Exception:

0 commit comments

Comments
 (0)