DEV Community

SAI RAM
SAI RAM

Posted on Originally published at anvilry.vercel.app

trelix v3.2.2 to v3.3.8: A GitHub App Already Hardened and Running in Production, and a Connector That Never Touches a Pixel

docker run --entrypoint trelix-mcp ghcr.io/sairam0424/trelix:3.2.1 --help returns exit code 127: command not found. Not a typo, not a stale tag — the published 3.2.1 image never contained trelix-mcp at all. The builder stage copied core trelix and nothing else, and .dockerignore's blanket packages/ exclusion would have blocked the MCP package even if someone had remembered the COPY line. A second, unrelated defect shipped in the same release: the published trelix-mcp console script ignored every flag you gave it — --help, -h, --version, anything — and silently launched the real MCP stdio server instead. Both were found the only way they could have been found: by installing the actual published artifacts and running them, not by reading a source tree that had never been wrong.

That's v3.2.2, and it's where this span starts. This article covers the thirteen tagged releases from v3.2.2 through v3.3.8 — 2026-08-26 through 2026-09-25 — picking up the day after the last article in this series ended at v3.2.1. v3.3.8 is the current release: tagged, pushed, marked "Latest," and — I checked this directly rather than trusting the changelog — live on PyPI right now.

Thirteen releases, one throughline

The prior span's throughline was a green test suite that never touched the bug it claimed to cover — a MagicMock standing in for an embedder, an all-ones attention mask making masked and unmasked math identical, a unit test that asserted a bug as its own specification. That defect class is still present in this span (more on it below), but it's no longer the dominant story. Two things sit ahead of it now.

The first is that the response to catching defects graduated from "write a regression test" to "build a system that makes this class of defect structurally unable to reach a tag again." The second is quieter and, I think, more interesting: almost everything genuinely new in this span — a diagram connector, an MCP protocol upgrade, a retriever-caching fix, a compression provider, an audit-log pruning mechanism — turned out to be sound at the code level when I went and checked it against source. What kept drifting was the layer just above the code: a renamed class the CHANGELOG never caught up to, a performance number with no artifact behind it, a guarantee that overstates what the code actually does. If the last article's angle was "the code lied to the tests," this one's second angle is closer to "the code is right, and the notes about the code keep being slightly wrong" — which is a more mature failure mode, and one that only shows up once the cruder failure mode has already been closed off.

The origin: a manual audit that automated itself

v3.2.2's fixes were correct but ordinary. What matters is what happened next. v3.2.3, two releases later, turned the exact manual "install the real published artifacts and run them" pass that had just caught two live defects into permanent CI: a real-subprocess E2E suite that spawns trelix-mcp as an actual OS process talking real stdio JSON-RPC, fresh-venv installs of all four published packages, a smoke-test-built-artifacts job that release.yml's publish job is now hard-gated on, a Docker check that trelix-mcp is actually present in the built image, and a Helm-lint check that the rendered image tag matches Chart.yaml. The same release fixed a real, separate bug along the way — a LIKE-wildcard escaping gap in path_filter-scoped BM25/grep queries, where unescaped _ and % in ordinary directory names were being read as SQL wildcards. Not injection (the queries were already parameterized), just a distinct semantics gap, closed with a shared escape_like_pattern() helper and an ESCAPE '\' clause.

The centerpiece is scripts/verify_release.py, a 742-line script with five independent check functions that each return a list of failures and never raise — so one category's bug can't hide another's results. check_pypi_installs does a fresh venv per package and a real pip install <pkg>==<version>, then a real console-script and import smoke test against the version string. check_docker_images pulls both the plain and -local tags and runs the exact --entrypoint trelix-mcp --version command that returned 127 on the unpatched 3.2.1 image — this check exists specifically because that command failed once, silently, in production. check_helm_chart checks out v<version> into a throwaway git worktree, never the caller's own working tree, and runs helm lint/helm template across all three store backends. check_github_release_binaries downloads and file(1)-checks all four platform assets and executes whichever one matches the current host, printing rather than silently skipping when it can't run a foreign-arch binary. check_security_audit runs pip-audit --format json on all four packages in a fresh venv and separately scans every wheel for .env*, .git-path, or credential/secret-named files that should never ship.

v3.2.4 wired this permanently into CI via .github/workflows/verify-release.yml: it waits for both release.yml and docker-publish.yml to go green for a pushed tag, then runs verify_release.py automatically and posts a PASS/FAIL comment, with the manual command still available for ad-hoc re-runs. I read the 99-line workflow directly — it uses env: passthrough rather than ${{ }} shell interpolation, matching docker-publish.yml's own injection-avoidance convention. v3.2.5, the same day, fixed a smaller instance of the identical class of gap: the frozen PyInstaller binary's embedder error message told users to pip install 'trelix[local]', which does nothing at all for a binary that never touches the host's Python or pip.

I ran the one check in this whole dossier that didn't rely on reading source: curl https://pypi.org/pypi/trelix/json. info.version is 3.3.8. The release history includes 3.2.2 through 3.2.5, then 3.3.0, 3.3.5, 3.3.6, 3.3.7, 3.3.8 — 3.3.1 through 3.3.4 are conspicuously absent from PyPI's own list, which I take to mean they were likely never tagged as standalone PyPI releases rather than any kind of cover-up; I didn't dig further and I'm flagging it rather than resolving it. trelix-mcp is confirmed in lockstep at 3.3.8 too, wheel and sdist both uploaded 2026-09-25T09:54 UTC. That's a directly executed, non-paraphrased result — the single strongest claim in this whole article, because it's the one number here that isn't source-reading, it's a live check against the real index.

A person holding a magnifying glass up to their eye, peering closely through it

v3.3.0: the migration that broke things on purpose

Every other release in this span is a patch. v3.3.0 is a deliberate minor bump with four real breaking changes, which is the honest way to signal "this will require you to change something," and I want to give it credit for that framing even where its own migration doc undersells it.

The four changes: the Python floor moved to >=3.12 across all three packages; the TRELIX_RETRIEVAL_FLARE_MAX_ITER env alias was removed outright, with zero remaining references anywhere outside docs; AWS_REGION is now hard-required for the Bedrock backend, raising ValueError where it used to silently default to us-east-1; and the Anthropic backend now silently no-ops on a temperature argument instead of forwarding it, logging a one-time warning instead. That last pair — a required env var and a silently dropped parameter — are exactly the two changes most likely to actually break a deployment on upgrade, and they are, by my own read of docs/migration/v3.2-to-v3.3.md, the two the migration guide doesn't mention at all. The doc covers three of the six real changes and its own closing section states the LLM-provider and MCP changes are "not yet released" — they shipped the same day as the doc, under the same version tag. If you're upgrading past v3.3.0, read the CHANGELOG, not the migration guide.

The release also introduces a reasoning-content abstraction across both cloud backends — and here's a naming error worth stating plainly because it'll cost you time if you don't know about it: the CHANGELOG calls this type ReasoningBlock. The actual class, which I confirmed directly in src/trelix/llm/client.py (line 29), is named ThinkingBlock. A repo-wide grep for ReasoningBlock across src/, packages/, and tests/ returns zero matches. ChatResponse.thinking_blocks: list[ThinkingBlock] plus a back-compat ChatResponse.thinking: str | None is what actually ships, and both the Anthropic and Bedrock backends build ThinkingBlock instances correctly, including the previously-dropped redacted_thinking variant. If you grep the codebase for the CHANGELOG's own name, you'll find nothing — use ThinkingBlock.

Separately, MCP gained SEP-2322 support: AgentResult.needs_input, set only on an explicit CLARIFY action from the agent loop, lets the ask_agent MCP tool wrap an ElicitRequest inside an InputRequiredResult when the agent genuinely needs clarification, resuming later via a dedicated answer-extraction path. Non-elicitation callers are unaffected — it's additive. This is the most solidly verified claim in the release, and also the one place the CHANGELOG's own claim doesn't hold all the way through: Bedrock's stream() method got a fix for dropped reasoning deltas, and the changelog describes it as fixing dropped reasoning and tool-use deltas together — "now logged at debug level instead of vanishing untraced." Reading the actual diff, only the reasoningContent half shipped; there is no branch at all for toolUse deltas, and a contentBlockDelta carrying incremental tool-call argument fragments still falls through with zero logging, exactly as it did before the fix. It's a small gap, but it's the kind of gap that only shows up when you read the code instead of the note describing it.

The parser-correctness class, one level deeper

The prior article's four-instance spine — a green suite that never exercised the defect — recurs here too, but this time it's the product of a deliberate audit rather than an accident someone happened to trip over: a correctness sweep hunting one specific bug pattern, qualified-name collisions, that pulled 19 independently confirmed defects across seven subsystems in v3.2.4 alone.

Two of them are worth walking through because they're the same shape at different layers. The installed tree-sitter-java grammar emits formal_parameters/formal_parameter for a Java record's component list, not the record_parameters/record_component node types the extractor had assumed — every record's component list had been rendering empty. The fix shares a single _extract_type_list_edges() helper across six call sites rather than patching the one spot that got noticed. Rust had the mirror problem: use foo::{Alpha, Beta} — the dominant real-world Rust import form — fell through to a generic single-name fallback because the code checked for use_tree_list/use_tree node types tree-sitter-rust doesn't actually emit; the real types are scoped_use_list/use_list. Pre-fix, that path would have produced the literal string "{Alpha, Beta}" as a bogus import name.

One layer up, in the federation retriever, make_scip_symbol_id() hashed only (package, version, qualified_name) — so two files sharing a qualified name, like two def main() entry points in different modules, collided, and the second was silently dropped via INSERT OR IGNORE. The fix folds file_path into the hash. And a fix in v3.3.4's chunker — where oversized symbols had their excess body silently discarded and replaced with a truncation marker, unrecoverable by retrieval — created a new collision surface one release later: multiple chunks could now share one symbol_id, and the fusion/dedup layer's key didn't yet account for that. v3.3.5's own fix docstring is candid about this being the same defect class recurring one abstraction level down from a prior bug, in code the same engineers had just touched days earlier. That's the honest version of "we fixed it" — not zero recurrence, but recognizing the recurrence immediately and naming it as such rather than treating each instance as a surprise.

A separate 94-agent chunking-strategy study, read in full, found trelix's one-symbol-per-chunk strategy underperforming alternative strategies by 3.57–5.64 percentage points of exact-match accuracy against a real published benchmark — and then rated its own recommendation "LOW-MEDIUM confidence a full redesign is worth building right now," because a redesign would break the 1-chunk-to-1-symbol cardinality assumption baked into the reranker and graph-context code — "the exact class of bug this session already found three separate times in adjacent code this same week." That's a research doc citing its own team's bug history as the reason to be cautious about its own conclusion, which is a genuinely unusual thing to see in a changelog-adjacent artifact, and I think it's a better outcome than either shipping the redesign on a hunch or ignoring the study.

trelix Code Review: not a Marketplace app, and one open question

There are two real, shipped ways to get trelix's review on a PR, and both post a check literally named "trelix Code Review." The first is a GitHub Actions workflow that trusts nothing beyond the repo's own GITHUB_TOKEN. The second is a standalone GitHub App — TypeScript/Express, webhook-driven, zero workflow YAML required in the installing repo, confirmed live right now on Railway (production environment, deployment from commit ac94e1c on main, instance status RUNNING). The tradeoff between the two is exactly what it looks like: the Actions path trusts nobody, the App path is more convenient but grants a real third party real permissions on your repo.

It is not, and should not be described as, a Marketplace app. infra/github-app/README.md says this outright, in words I'm quoting rather than paraphrasing: "Not claimed: GitHub Marketplace listing. This App is installable and hardened, not Marketplace-verified." docs/ROADMAP.md lists the Marketplace listing as an explicit open backlog item, gated on needing roughly 100 installations before GitHub will even review it for the listing — an adoption gate, not an engineering one.

The security posture underneath, which I read at the file level rather than taking on faith: webhook signature verification uses timingSafeEqual via @octokit/webhooks-methods and rejects unauthorized requests before the payload body is even read; installation tokens are minted and cached per-config in a WeakMap, with JWT-mode auth correctly not cached; PR checkouts clone into a temp workspace and fetch refs/pull/<n>/head, which is the correct ref for fork PRs, with the token passed only via a GIT_ASKPASS environment variable — never in a URL, argv, or git config, where it could leak into logs or process listings; and there's a 25MB payload cap applied before signature verification, which is the only defense against an oversized unsigned body. As of v3.3.8, a review that times out or crashes now always posts a completed Check run instead of leaving the PR with no status at all — timed_out conclusion when the process was SIGTERM'd, neutral otherwise. That's a real, shipped fix for a real silence-on-failure gap, and it's a good instinct for a system whose entire value proposition is a status check people trust.

What's still open, and what I want to be careful not to resolve in either direction just because it would make a tidier ending: there's a documented, not-yet-confirmed-resolved concern that the App's own review of trelix's own large codebase has been getting SIGKILLed before indexing finishes, suspected to trace back to review-runner.ts's hardcoded 5-minute INDEX_TIMEOUT_MS/REVIEW_TIMEOUT_MS. I checked those constants directly — they're unchanged at 5 minutes, in this span and since. But I also checked every CHANGELOG line, every ROADMAP line, and the git log for any commit connecting those two constants to a SIGKILL-during-self-review incident, and found nothing. The only SIGKILL mentions anywhere in the App's code concern a completely different scenario — OOM-killed processes leaving stale temp workspaces behind, which a sweepStaleWorkspaces() cleanup at boot handles. So: the timeout constants are exactly as short as the concern says, but nothing in the repository substantiates that they're actually the cause of a self-review failure, or that the failure is still happening at all. I'm stating this as an open question because it is one, not because I want to end this section on a cliffhanger. It should not be confused with a different, already-closed issue in the same App: a missing TRELIX_EMBEDDER_PROVIDER=azure env var on Railway, which caused indexing to silently fall back to a never-installed local embedder and fail — that was fixed on 2026-09-22 via a deployment config change, not a code change, and it's fully resolved. These are two unrelated bugs at two unrelated layers; only one of them is closed.

The .drawio connector: diagrams as text

v3.3.6 shipped a fifth connector alongside Jira, TestRail, Xray, and Linear: trelix connector sync <repo> diagram, for local .drawio (diagrams.net) files. The mechanism is more mundane than it sounds, in the best way — every shape in a drawio file's mxGraph XML carries its label in a plain value="..." attribute, and every edge is an explicit <mxCell edge="1" source=... target=...>, so the whole diagram is fully text-describable without any vision model. The connector truncates the XML to 20,000 characters and sends it through the same text-only chat client every other LLM call site in trelix already uses. Raster images — PNG, JPG — are explicitly out of scope; there's no glob or handling for them anywhere.

v3.3.7 fixed a real bug in the fallback path: when captioning failed, the connector used to discard the diagram's real content entirely and report just a character count. Now it regex-extracts actual mxCell value="..." shape labels, HTML-unescapes before stripping inline tags (the order matters, since drawio escapes attribute values, so entities have to decode before tags can be safely stripped), de-duplicates in first-seen order, caps at 50 labels, and only falls back to the old length-only message when literally zero labels exist. It's a small fix but a real one — the difference between "the diagram failed to caption, here's 4,200 characters" and "the diagram failed to caption, here are the 12 real shape labels it contains" is the difference between a useless fallback and a usable one. There's no diagram-specific config or env var; it rides whichever LLM provider is already configured, and the artifact linker treats a diagram exactly like a Jira ticket — same edge shape, same matching logic, no special-cased code path anywhere.

An animated isometric architectural line diagram assembling itself piece by piece

Performance: real numbers, and one number that isn't

v3.3.8 fixed a real inefficiency: trelix-mcp, the LangChain retriever, and the LlamaIndex retriever all used to rebuild a fresh Retriever object on every single call. They're cached now — module-level with a lock in the MCP server, invalidated correctly on reindex, per-instance in both framework adapters. The mechanism is sound and I'd recommend the fix without reservation. What I can't recommend repeating is the specific number attached to it: the CHANGELOG cites a "24.5x–52.7x measured speedup," and that figure appears nowhere except the CHANGELOG's own prose — not in the commit message I pulled directly, not in the PR, not in any test. The commit itself says only that three consecutive calls in one session took 13-16 seconds, then 5, then 5, with zero warm-up benefit — no multiplier, and no recorded post-fix warm baseline anywhere to derive one from. The fix is real; the multiplier isn't backed by anything outside the CHANGELOG's own unsupported prose, so I'm not repeating it as measured.

The rest of this theme holds up better. trelix-mcp's and the graph community-detection code's use of nx.pagerank now catches ImportError and degrades to a uniform-score fallback instead of crashing, because scipy is only imported lazily on first call and the frozen binary deliberately excludes it — a real crash class closed with two small, defensive catches. Qdrant gained int8/binary quantization support, with one small flag: the commit claims verification against qdrant-client==1.19.0, but the lockfile currently pins 1.18.0. Not a contradiction — a dev-venv version never captured in the lockfile is the more likely explanation than anything adversarial — but not independently confirmable either, so treat "verified against 1.19.0" as unconfirmed rather than false. A direct Cohere embedder landed using cohere.ClientV2 rather than routing through Bedrock, with a 96-item batch limit. And an abstractive compression provider shipped with a real accuracy gap between what the CHANGELOG claims and what the code guarantees: the CHANGELOG says the signature and docstring line are "always kept byte-for-byte." The actual _must_keep() logic only locates and preserves the declaration line itself — the docstring on subsequent lines is forwarded to the LLM as context, with no verbatim guarantee, and can come back paraphrased. The test suite's own docstring gets this right where the CHANGELOG doesn't: "the declaration line is ALWAYS kept verbatim," singular. If you're relying on this feature to preserve docstrings exactly, don't — that's what the separate extractive compressor genuinely does.

A rocket igniting and lifting off from the launch pad in a cloud of exhaust

Security: two CRITICAL CVEs and an audit trail that undersells itself

The litellm floor moved to >=1.90.2, closing two CVEs the project's own pins cite as CVSS 9.8 — a Host-header authentication bypass and an unauthenticated SSTI leading to RCE — plus a third SSRF issue via nested user_config. I can confirm these citations are in the repo's own dependency comments; I could not independently cross-check the CVE IDs against GitHub's live advisory database from this sandbox, so I'm attributing the "two CRITICAL, CVSS 9.8" framing to trelix's own release notes rather than an outside source I verified myself. Separately, pydantic-settings CVE-2026-58203 was patched by a floor bump, and I confirmed the vulnerable feature — secrets_nested_subdir — was never actually called anywhere in the codebase, so this one was zero real exposure even before the bump; a clean case of patching a dependency that was never actually exploitable through this code.

The most substantial security addition is trelix audit prune — a real hash-chain watermarking implementation for pruning old audit-log rows without breaking chain verification, with an --dry-run mode that's confirmed not wired into any in-process scheduler. What struck me reading it is that the module's own docstring is more conservative than the CHANGELOG's framing: it states plainly that this is "tamper-evident, not tamper-proof," and it enumerates exactly which tamper shapes remain undetectable — a total wipe that also clears SQLite's own internal sequence table, for instance. A security feature whose own documentation undersells its guarantees relative to the marketing copy around it is the rare direction I'm happy to see the drift run in.

An animated shield illustration with a glowing lava-lamp pattern moving inside it

The smaller robustness releases

Two releases of CLI/API/extension hardening are worth a fast pass. v3.3.1 fixed four real VS Code extension bugs: a JSON.parse crash whenever the MCP client returned a plain-text error instead of JSON, a path escape hatch for GUI-launched editors whose PATH excludes pip/uv, inline symbol links in chat responses, and a CodeLens replacing a global QuickPick for blast-radius visualization. v3.3.7 fixed a .vscodeignore gap that had been shipping a local vector database file and gigabyte-plus of sourcemaps inside the packaged extension — vsce ignores .gitignore entirely once .vscodeignore exists, so the two files having drifted apart had real consequences. The same release moved four CLI/REST failure modes from raw tracebacks or bare 500s to labeled, structured errors: eval/eval-synthesis/migrate-vectors now print one-line labeled errors instead of stack traces; a missing golden file for synthesis eval now raises FileNotFoundError instead of silently returning zero-filled metrics; the REST API's DimensionMismatchError and ImportError now return structured JSON 500s instead of Starlette's plain-text default; and static-token auth now checks Authorization: Bearer independently of X-Trelix-Api-Key, via hmac.compare_digest, closing a path where the Bearer header was silently ignored.

The pattern behind the corrections

I want to name the pattern across this article's corrections rather than let them read as scattered nitpicks, because I think they're one thing: ReasoningBlock vs. ThinkingBlock, the half-shipped toolUse-delta logging claim, the unaudited 24.5x-52.7x figure, the docstring-verbatim overstatement, a migration guide that's stale relative to its own release date, a docs/WHY_TRELIX.md version-scope stamp that still reads v3.3.7 while the package is at v3.3.8 — none of these are code bugs. Every one of them is the layer just above the code: the sentence describing what the code does, written slightly faster or slightly more optimistically than the code itself justifies. That's a different failure mode than a MagicMock hiding a broken embedder, and it's arguably a sign of health rather than decline — it only becomes the dominant story once the release-verification discipline from earlier in this span has already closed off the cruder failure mode of a defect reaching a tag at all. It's also, not coincidentally, exactly the discipline this article itself needed applied to it: this dossier exists because the same standard — check the sentence against the source, not against the intent — got turned on the release notes themselves, and turned up a correction on nearly every theme it touched.

What v3.3.8 actually means for someone upgrading today

If you're upgrading from anywhere before v3.3.0, read the CHANGELOG for that release specifically rather than the migration guide — the guide is missing the AWS_REGION requirement and the silently-dropped temperature parameter, which are the two changes most likely to actually break your deployment. If you're grepping the codebase for the reasoning-content type, look for ThinkingBlock, not ReasoningBlock. If you're evaluating the retriever-caching fix, trust the mechanism and not the specific multiplier. If you're relying on abstractive compression to preserve a function's docstring exactly, it doesn't — only the declaration line is guaranteed verbatim. And if you're deciding whether to install the GitHub App or rely on the Actions workflow for PR review, both post the same "trelix Code Review" check, but only one of them asks you to trust a third party — and if you run the App against a codebase the size of trelix's own, know that a timeout-related self-review concern from a week ago hasn't been confirmed fixed, or confirmed to still be happening either.

None of that is a reason not to upgrade. The live PyPI check I ran directly — not read, not paraphrased, executed — confirms v3.3.8 across all four packages, published in lockstep on 2026-09-25. The release-verification infrastructure built in v3.2.3 and v3.2.4 means that check itself, along with a Docker --version smoke test and a Helm chart lint, now runs automatically against every tag before it's allowed to publish, which is a meaningfully different guarantee than the source tree alone ever gave you. What changed across these thirteen releases isn't that trelix stopped shipping defects — the parser sweep alone found 19 of them in one release. What changed is that the mechanism for catching what a green test suite misses stopped being a person doing it once, by hand, out of frustration, and started being a system that does it on every tag, whether anyone remembers to ask or not. The part that hasn't caught up yet is the sentence describing the fix, which is a smaller problem to have than the one this project started with nine weeks ago, and a more honest place to end an audit than pretending it isn't there at all.

Top comments (0)