Files
zzet--gortex/bench/issue40_context_repro.py
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

179 lines
6.8 KiB
Python

#!/usr/bin/env python3
"""
Reproduction for issue #40 — "Claude Code eats up context when using gortex".
The report's core, *measurable* claim is:
- During plan implementation, files get read in FULL via gortex's read_file /
get_editing_context, which is token-expensive.
- compress_bodies:true and/or search_text are far cheaper, but nothing forces
(or even nudges toward) them — read_file defaults to compress_bodies:false.
This script confirms/disproves the *measurable* part by driving the REAL tools
through the running daemon (via `gortex mcp --proxy`) and comparing the wire
cost of three access patterns on the same set of files:
1. read_file (full bodies — the "eats context" path)
2. read_file compress_bodies:true (signatures + structure only)
3. search_text (locate call sites, no body read at all)
Token figures are estimated at ~bytes/4 (the standard rough heuristic; Claude's
real tokenizer differs but the RATIO between patterns is what matters and is
tokenizer-stable). The script reports raw bytes too, so nothing hinges on the
estimate.
Usage:
python3 bench/issue40_context_repro.py [GORTEX_BIN] [file ...]
Defaults: ./gortex and a handful of ~14-24KB Go files (≈ the reporter's C++
file sizes). Pass a repo-prefixed or absolute path per file (e.g.
gortex/internal/resolver/external_calls.go).
"""
import json
import subprocess
import sys
import threading
GORTEX_BIN = sys.argv[1] if len(sys.argv) > 1 else "./gortex"
FILES = sys.argv[2:] or [
"gortex/internal/resolver/external_calls.go",
"gortex/internal/mcp/tools_lsp.go",
"gortex/internal/agents/claudecode/plugin.go",
"gortex/internal/parser/languages/swift.go",
]
# A literal that recurs across the repo — the search_text "locate the call
# sites" pattern the reporter says should have been used instead of reads.
SEARCH_QUERY = "zap.Error"
def approx_tokens(nbytes: int) -> int:
return round(nbytes / 4)
class MCP:
"""Minimal newline-delimited JSON-RPC client over `gortex mcp --proxy`."""
def __init__(self, binary):
self.p = subprocess.Popen(
[binary, "mcp", "--proxy", "--log-level", "error"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, bufsize=1,
)
self._id = 0
# Drain stderr so a chatty daemon can't dead-lock the pipe.
self._err = []
threading.Thread(target=self._drain_err, daemon=True).start()
def _drain_err(self):
for line in self.p.stderr:
self._err.append(line)
def _send(self, method, params=None, notify=False):
msg = {"jsonrpc": "2.0", "method": method}
if params is not None:
msg["params"] = params
if not notify:
self._id += 1
msg["id"] = self._id
self.p.stdin.write(json.dumps(msg) + "\n")
self.p.stdin.flush()
if notify:
return None
return self._read_result(self._id)
def _read_result(self, want_id):
while True:
line = self.p.stdout.readline()
if not line:
raise RuntimeError(
"daemon closed the connection.\nstderr:\n" + "".join(self._err[-20:]))
try:
msg = json.loads(line)
except json.JSONDecodeError:
continue # skip log noise that leaked onto stdout
if msg.get("id") == want_id:
if "error" in msg:
raise RuntimeError(f"RPC error: {msg['error']}")
return msg.get("result")
def initialize(self):
self._send("initialize", {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {"name": "issue40-repro", "version": "0"},
})
self._send("notifications/initialized", notify=True)
def call(self, name, args):
res = self._send("tools/call", {"name": name, "arguments": args})
# Concatenate all text content blocks — that is what lands in the model's
# context window.
parts = []
for block in (res or {}).get("content", []):
if block.get("type") == "text":
parts.append(block.get("text", ""))
return "".join(parts)
def close(self):
try:
self.p.stdin.close()
except Exception:
pass
self.p.terminate()
def main():
mcp = MCP(GORTEX_BIN)
try:
mcp.initialize()
print(f"Driving real tools via `{GORTEX_BIN} mcp --proxy`\n")
rows = []
tot_full = tot_comp = 0
for path in FILES:
full = mcp.call("read_file", {"path": path})
comp = mcp.call("read_file", {"path": path, "compress_bodies": True})
bf, bc = len(full.encode()), len(comp.encode())
tot_full += bf
tot_comp += bc
save = 100 * (1 - bc / bf) if bf else 0
rows.append((path, bf, bc, save))
name_w = max(len(p) for p, *_ in rows)
print(f"{'file':<{name_w}} {'full B':>9} {'compress B':>11} "
f"{'full ~tok':>10} {'compress ~tok':>13} {'saved':>6}")
print("-" * (name_w + 60))
for path, bf, bc, save in rows:
print(f"{path:<{name_w}} {bf:>9,} {bc:>11,} "
f"{approx_tokens(bf):>10,} {approx_tokens(bc):>13,} {save:>5.0f}%")
tot_save = 100 * (1 - tot_comp / tot_full) if tot_full else 0
print("-" * (name_w + 60))
print(f"{'TOTAL':<{name_w}} {tot_full:>9,} {tot_comp:>11,} "
f"{approx_tokens(tot_full):>10,} {approx_tokens(tot_comp):>13,} {tot_save:>5.0f}%")
# Pattern 3: locate call sites instead of reading bodies at all.
# Cost scales with match count, so the honest figure is per-match.
st = mcp.call("search_text", {"query": SEARCH_QUERY, "limit": 100})
try:
n_matches = json.loads(st).get("count", 0)
except json.JSONDecodeError:
n_matches = st.count("path:")
bs = len(st.encode())
per = approx_tokens(bs) / n_matches if n_matches else 0
print(f"\nsearch_text(query={SEARCH_QUERY!r}): {n_matches} sites located in "
f"{bs:,} B (~{approx_tokens(bs):,} tok ≈ {per:.0f} tok/site) — "
f"line-precise file:line, zero bodies read")
print("\nVerdict inputs:")
print(f" • Full reads cost ~{approx_tokens(tot_full):,} tok for {len(FILES)} files.")
print(f" • compress_bodies:true would cost ~{approx_tokens(tot_comp):,} tok "
f"({tot_save:.0f}% less) — same signatures/structure.")
print(f" • read_file's DEFAULT is compress_bodies:false → the expensive "
f"path is the default path.")
finally:
mcp.close()
if __name__ == "__main__":
main()