Skip to content

Context-efficiency backlog

Status: Current | Last reviewed: 2026-08-12

A prioritized record of files large enough to cost an agent (or a human) real time on every edit, audited on 2026-08-08 but not executed. Read this before proposing a split so you don't re-measure what's already measured or walk into a risk someone already found.

This is the file-shape sibling of config-realign-backlog.md — that page tracks shapes inside the config JSONB; this one tracks the modules that are hard to work in. Nothing here is a bug. Every file listed works; the cost is that changing one means reading thousands of lines first.

What the 2026-08-08 pass shipped, and what it deliberately left

The audit found three causes of context thrashing on large changes. Two were fixed:

  1. Root CLAUDE.md was 208 KB and loaded in every session — ~200 KB of it in 75 "Critical invariants" bullets, most of them app-specific. Fixed by moving app-specific bullets into per-app apps/<app>/CLAUDE.md files (which Claude Code loads only when the work is in that directory) and leaving a one-line index plus the cross-cutting invariants in root. Root is now ~32 KB. See the convention note in docs/README.md.
  2. Generated and vendored trees polluted every search — the repo-root .ignore now excludes ces-v1-py/ (~117 K lines of GAPIC output), the three generated config schemas, and registry/ (shadcn copies). Files stay in git and readable by explicit path; only search results change. (The vendored tree's build output was a separate problem, fixed on 2026-08-12 — see below.)
  3. Giant hot files — deferred to this page. Each split is a real refactor with real regression risk, and several are load-bearing for tests that assert on source structure rather than behaviour (see the source-introspection trap).

Splitting a file is worth doing when it is both large and frequently edited — size alone is tolerable if nobody touches it. The table below is ordered on that product.

What the 2026-08-12 dead-code pass removed

Before splitting the two editor files, everything in them that no longer ran was deleted. This is not a refactor — nothing moved, and no reachable line changed — so it is worth doing first: it is the cheapest reduction available, and it stops a split from carrying dead code into a new module where the next reader has to re-derive that it's dead.

File Before After What went
FlowEditor.tsx 5,236 4,542 A 413-line /* … */ block holding the pre-useLayout manual layout algorithm; a useEffect that logged every node on every change; 12 console.log traces; four local MAIN_PIPELINE_X = 400 redeclarations shadowing the module constant; six unreferenced helpers (getEdgeType, flowElementsToTranscriptBranch, pipelinesEqual, handleAddService, handleAddParallelGroup, hasInitialCenteredRef)
ServiceConfigForm.tsx 5,774 5,690 The {false && otherFields.length > 0 && (…)} "Additional Configuration" editor — a raw key/value form over any unrecognised config key, disabled by a literal false — and the otherFields derivation that fed only it

The two console.error calls that report a missing start/end node stayed: they fire on a real invariant violation, not on every render. handleFieldChange/handleRemoveField stayed too — the hidden block was not their only caller, contrary to what the deletion plan assumed.

One thing left in place deliberately: pipelineToFlowElements takes a fourth lifecycle parameter that its body never reads, and two call sites compute a value to pass to it. That is dead plumbing, but removing it means deleting those computations and their useMemo dependencies, which changes how often the memo recomputes. It is a behaviour question, not a deletion.

What the 2026-08-12 build-artifact pass removed

Ignoring a tree in .ignore hides it from search; it does nothing about what the tree costs everywhere else. apps/ai-orchestrator was tracking 82 files / 2.9 MB of pure build output for its vendored Google CES client, plus three archives nothing installs:

Removed Was Why it was safe
ces-v1-py/build/** (73 files, 52,625 lines) setuptools' staging copy of the google/ tree setup.py builds packages from find_namespace_packages() filtered to startswith("google"), so build.lib.google.… is excluded from every install
ces-v1-py/google_cloud_ces.egg-info/** (6 files) setuptools metadata Regenerated on each build; the dev venv holds a real (non-editable) copy in site-packages, so nothing reads the source-tree metadata
ces-v1-py/ces-v1-py.tar.gz (256 KB) an sdist of the directory it sits in Unreferenced repo-wide
ces_v1_ces-v1-py.tar (592 KB) another snapshot of the same Unreferenced repo-wide
nvidia_ace-1.2.0-py3-none-any.whl (32 KB) a vendored NVIDIA wheel Not in pyproject.toml, not in uv.lock, not imported anywhere, and not installed in the venv — recoverable from commit a9109bf4 if a future ACE integration wants it

An ignore rule is not a deletion. The root .gitignore had listed apps/ai-orchestrator/ces-v1-py/build/ since the tree was vendored, and it did nothing: the 73 files were committed before the rule, and gitignore never applies to an already-tracked path. They shipped in every clone regardless. After writing an ignore rule, check git ls-files <path> — if it prints anything, the rule is decorative until you git rm -r --cached. The same sweep found libs/reasoning/trueserve_reasoning.egg-info/ (4 files) tracked and matched by no rule at all; libs/reasoning is never pip-installed (consumers add libs/ to sys.path, and the Dockerfiles COPY libs/reasoning), so that metadata was read by nothing. Both are untracked now, and the root rules cover libs/*/*.egg-info/ as well as apps/*/*.egg-info/. Note a * never crosses a /, so neither reaches a package nested two levels down — that is why ces-v1-py's egg-info needed its own rule in the service's own .gitignore.

The source stays tracked and is load-bearing: pyproject.toml declares google-cloud-ces = { path = "ces-v1-py" } and the Dockerfile COPYs the directory whole. Deleting the build output does not affect that — verified by rebuilding the wheel from the stripped tree (uv build ces-v1-py --wheel), which is what uv sync --frozen does in the image. Ignore rules now live in apps/ai-orchestrator/.gitignore so the artifacts can't come back, and they cover *.tar/*.tar.gz/*.whl for the whole service: nothing in that tree is ever installed from a local archive (deps resolve from uv.lock; the Krisp wheels are produced by prepare_krisp_runtime.py at image-build time).

Worth doing before any file split, for the same reason as the dead-code pass: it is the cheapest reduction available, and it needs no judgement about behaviour.

The source-introspection trap (read this first)

Five test files assert on the source text or AST of the modules they cover, because the behaviour they guard cannot be exercised in a unit test — it needs a live gRPC queue, a connected session, or a fully built pipeline. Moving code these tests read will break them, and not always as a clean assertion failure:

Test Reads How it breaks on a split
test_payment_mode_controller.py:936 ast.parse(inspect.getsource(universal_builder)) Walks the module tree for _build_* functions and asserts _insert_payment_controller precedes both DTMFInputHandler() and chain.append(user_aggregator) within each one. Also asserts the exact set of chain builders is {_build_branch, _build_cascade_service_chain, _build_s2s_service_chain} — moving one out of the module fails the set equality, not just the ordering.
test_copilot_builder.py:1134 Path(...)/"lib/pipecat/frame_processors/conversation_api_publisher.py", then text.index("elif isinstance(frame, SuggestionFrame):") Hardcoded path + str.index on an anchor string. A split raises ValueError: substring not found (or FileNotFoundError), not an assertion error — so the failure won't read as "the split broke a placement guard".
test_copilot_builder.py:1171 same file, text.index("async def _publish_inline_translation") then slices to the next async def Same failure mode. Asserts the body allocates uuid.uuid4() and passes source="translation" with no variant=.
test_copilot_builder.py:1795 inspect.getsource(CopilotPipelineBuilder.build_processors) Survives a move of the method, fails if its body is decomposed.
test_translation_transcripts.py:325 inspect.getsource(translation_builder.TranslationPipelineBuilder) Survives a move of the class, fails if logic moves to a helper module.

The technique is deliberate and documented in the tests themselves — the comment at test_payment_mode_controller.py:925 explains that asserting on source strings would only prove the lines exist somewhere in a 7,000-line module, not that they sit in the same function in the right order on the path that runs. Do not "fix" these tests by weakening them. A split that moves what they read must move the assertion to the new home and keep the same strength. The two Path(...) ones should also be converted to inspect.getsource() on the new module so the next move fails loudly instead of cryptically.

Backlog

Ordered by size × churn (commits in the 3 months to 2026-08-08). Each entry: Seams / Risk / Notes.

1. universal_builder.py — 7,013 lines, 87 methods, 11 commits

apps/ai-orchestrator/lib/pipecat/pipeline/builders/universal_builder.py

Seams (a) service-config translation — _build_service_specific_config, _translate_service_config, and the S2S instruction promotion → a service_config_translation.py; (b) the _insert_* processor-insertion family → a chain_assembly.py; (c) _harvest_greeting + greeting cleanup → greeting.py.
Risk High, and not where you'd expect. The payments AST test above asserts placement within each _build_* function and asserts the exact set of chain builders in this module. Seam (b) moves _insert_payment_controller out of the module the test parses — that test must move with it and keep asserting per-builder ordering. Seam (a) is the safest of the three: translation is mostly pure config-in/config-out.
Notes The highest-value entry — biggest file, near-highest churn. Read apps/ai-orchestrator/CLAUDE.md first; ~20 of its invariants are about this builder, several describing silent failures (Bedrock leading-turn, ADK InstructionProvider, S2S frame re-push, the one-shot greeting instruction). Do seams one at a time, each its own commit.

2. ServiceConfigForm.tsx — 5,690 lines, 20 components, 9 commits

apps/ai-manager/components/ServiceConfigForm.tsx

Seams Already 20 components in one file, only one of them exported — so 19 are file-local and movable without touching a single import elsewhere. BackgroundAudioSection (1121), AgentIdentitySection (1252), NodeCapabilitiesAndBehaviors (1328), EndSessionAnalysisPanel (2331), ConfigFieldsEditor (2743) → components/service-config/*.tsx. Keep the exported ServiceConfigForm shell plus its form wiring and uniqueStageId derivation in place.
Risk Low-to-moderate, and the lowest risk-per-line-saved on this page — the 19-local-components fact means the module's public surface doesn't change. Watch prop threading (ConfigFieldsEditor takes 15 props) and the section prop that gates which blocks render. Verify collab re-render behaviour and pipelineStructureSignature after moving.
Notes Best first candidate if someone wants to establish the pattern cheaply.

3. http_api.py — 5,135 lines, 41 routes, 14 commits

apps/conversation-api/http_api.py

Seams Split into APIRouters by domain — conversations, embed, A2A, replay.
Risk Moderate. All 41 decorators are @app. (zero @router. today), so this is a genuine conversion, not a regrouping — every route changes decorator and gains an include_router call. No test asserts the route inventory, so a route silently dropped in the move would not be caught: diff sorted(app.openapi()["paths"]) before and after and require it identical.
Notes Highest churn on the page (14 commits) — the file people actually fight with most. Known gotcha: on FastAPI 0.138+, include_router appends a lazy _IncludedRouter, so a mounted router does not appear in app.routes and looks missing; verify via app.openapi()["paths"].

4. conversation_api_publisher.py — 4,999 lines, 56 methods, 8 commits

apps/ai-orchestrator/lib/pipecat/frame_processors/conversation_api_publisher.py

Seams Extract per-frame-family handlers (_publish_*) into sibling modules, leaving the frame dispatch in place.
Risk High — two tests hardcode this file's path and str.index() into its body (see the trap table). They fail with ValueError/FileNotFoundError rather than a readable assertion. _publish_tool_call must remain the single chokepoint for tool-call emission (the A2UI repair invariant in apps/conversation-api/CLAUDE.md — malformed A2UI is repaired on the way out, and a second emission path bypasses the repair).
Notes Convert those two tests to inspect.getsource() on whatever module ends up owning the code, as part of the same commit.

5. WorkflowGraphTab.tsx — 4,369 lines, 9 commits

apps/ai-manager/components/workflow-editor/WorkflowGraphTab.tsx

Seams Same shape as FlowEditor — lift pure graph computation out, keep the React shell.
Risk Moderate; not yet audited at the function level. Measure before committing to seams.

6. FlowEditor.tsx — 4,542 lines, 5 commits

apps/ai-manager/components/FlowEditor.tsx

Seams computeJunctions (line 419, 258 lines) and computeFrameFlow (line 677, 100 lines) → lib/flow/. Verified pure: neither calls a hook, and neither is referenced anywhere else in apps/ai-manager, so they can move with no import churn.
Risk Lowest on this page. Purity is not incidental — it's an invariant: both the junction diamonds and edge colouring are view-only overlays that must never mutate stored config (see apps/ai-manager/CLAUDE.md). Moving them into pure modules makes that structurally obvious and unit-testable.
Notes Lower churn than the entries above, but 358 lines of provably-pure logic is the cleanest extraction available. Good pairing with entry 2 for a first pass.

7. agents/[agentId]/page.tsx — 3,046 lines, 79 hook calls, 9 commits

apps/ai-manager/app/(main)/agents/[agentId]/page.tsx

Seams Extract custom hooks: preflight polling, publish dialog (including the CCaaS availability list), canvas prefs.
Risk Moderate. 79 hook calls in one component means ordering matters — extract one hook per commit and check the React Hooks lint rule stays clean. The publish dialog owns the CCaaS allow-list write, which is a default-deny security gate (root CLAUDE.md); don't let its atomicity with the publish body regress.

8. ToolSelector.tsx — 2,768 lines, 10 commits

apps/ai-manager/components/ToolSelector.tsx

Seams Three inline dialogs are file-local and independently movable: InlinePythonToolDialog (1679), InlineKnowledgeToolDialog (1891), InlineApiToolDialog (2165).
Risk Moderate. High churn for its size. Inline-tool dialog updaters must use functional setDraft(prev => …) and never setDraft({...draft}) — see apps/ai-manager/CLAUDE.md; a move that "tidies" a spread reintroduces a lost-update bug that only shows under fast typing.

Not files: two docs that need TOCs

Lower priority, and cheap. connect-scv-translator.md is 109 KB and universal-pipeline-builder.md is 89 KB. Neither has a top-of-file table of contents, so an agent that needs one section has no way to know that in advance and reads the whole file. Adding a TOC with a one-line summary per anchor turns a 100 KB read into a 2 KB read plus a targeted section. Do this before splitting either doc — a TOC keeps the single-file advantage while removing most of the cost.

See also