Umwelten System Map — May 2026
A system-architect-level audit of the codebase: what the fundamental pieces are, how they fit together, where reality has drifted from the documented design, and what to do about it. Based on a parallel deep-dive across six subsystems.
0. Second pass — what the first pass missed
The six parallel subagents mapped subsystems against CLAUDE.md as the reference. On re-examination, the bigger problem is that CLAUDE.md itself is more broken than reported, and several artifacts didn't come up at all. Five additions:
0.1 The package layout in CLAUDE.md is wrong about which packages exist
CLAUDE.md lists six packages: core, server, evaluation, habitat, ui, cli. Reality: cli, core, evaluation, habitat, protocols, sessions, ui, umwelten.
@umwelten/serverdoes not exist — renamed to@umwelten/protocols(commit 8f44b16 "Aggressive package-layout simplification"). CLAUDE.md still references@umwelten/serverfour times.@umwelten/sessions— not in CLAUDE.md at all. It ownssessionsCommand,browseCommand,introspectCommand, and the introspection data layer; the CLI depends on it.@umwelten/protocols— not mentioned at all, despite being where MCP / A2A / OAuth all live.umweltenmeta-package — mentioned in passing; its actual job (re-export everything for backwards compat) isn't described.
The documented DAG (core ← server ← evaluation ← habitat ← ui ← cli) is missing two nodes. True DAG is closer to: core ← protocols ← (sessions, evaluation) ← habitat ← ui ← cli, with umwelten as a meta-barrel.
0.2 The Exploration pipeline isn't undocumented — it's in CONTEXT.md, not CLAUDE.md
The first pass flagged interaction/{projection,promotion,reflection,knowledge}/ as a "hidden feature pipeline." In fact, CONTEXT.md at the repo root is the formal domain glossary and defines every term these directories implement: Interaction, Source Session, Exploration, Saved Exploration, Project Fact, Memory, Reflection, Saved Reflection, Exploration Browser. ADR-0001 (docs/adr/0001-project-pi-session-trees-as-explorations.md) commits to the model.
So the drift is the opposite of what was reported: the code is right, the language is documented — but CLAUDE.md (the doc agents read) doesn't reference any of it. Two domain docs (CONTEXT.md + CLAUDE.md) describing the same system in different vocabularies is the actual problem.
0.3 There are at least four canonical architecture documents, all stale in different ways
- CLAUDE.md — agent instructions + module map. Wrong about packages (§0.1), wrong about
bridge/, wrong aboutreporting/andintrospection/locations. - CONTEXT.md — domain language. Internally consistent but unreferenced from CLAUDE.md.
- docs/architecture/overview.md — another high-level map. Uses broken
@umwelten/...link syntax ([habitat](@umwelten/habitat/habitat.ts)isn't a valid link), but otherwise mostly aligned with reality. - AGENTS.md — thin pointer to GitHub Issues + CONTEXT.md.
Plus eight more architecture docs under docs/architecture/ of varying staleness. session-record-introspection.md exists and accurately describes session-record/ — the module CLAUDE.md fails to mention.
0.4 Tooling artifacts not surveyed
knip.jsonis configured.pnpm knipwould catch the dead-code items mechanically. Run it.vitest.config.tsusessingleThread: true. Test counts: core 65, habitat 18, evaluation 15, ui 6, sessions 2, cli 2, protocols 0. Protocols has zero tests despite holding OAuth-bearing modern code.mise.tomldefineshabitat-build/habitat-run/habitat-serve/gaiatasks the CLI doesn't document. These are the recommended entry points.- Root
package.jsondeclaresdocs:dev/docs:build— there's a VitePress site underdocs/(homepage: umwelten.thefocus.ai). Not mentioned in CLAUDE.md or the first-pass report.
0.5 The umwelten meta-package is doing real work that nobody flagged
packages/umwelten/src/index.ts is 349 lines of curated re-exports across every package — the public API for the npm-published umwelten package. When CLAUDE.md's "public barrel" is mentioned, that's packages/core/src/index.ts, but the npm-published barrel is the meta-package. Anything missing from packages/umwelten/src/index.ts is invisible to npm install umwelten users.
packages/umwelten/src/mcp-serve.ts is a backwards-compat re-export shim explicitly acknowledging a recent rename from @umwelten/server to @umwelten/protocols. Evidence the package rename is incomplete in the docs.
Things still not investigated in depth
- 16 examples under
examples/, several non-trivial (local-providers5272 LoC,memorization3075,mcp-chat2812,model-showdown2780,jeeves-bot1357). Reference implementations / fixtures the subsystem passes didn't enter. If any deep-importsmemory/, the legacy MCP client, ordiscord-routing.ts, those are real migration constraints. docs/architecture/electron-shell.mddescribes a planned desktop shell wrappingcontainer-server. If active, the "collapse the 4 HTTP servers" recommendation has to preservecontainer-server.tssurface area.docs/architecture/promote-tools-to-mcp.mdis an open plan to bridge habitat tools to standalonemcp-serveMCP servers. Directly affects the "habitat/mcp-local-server.ts duplicates mcp-serve/mcp-handler.ts" finding — actively planned work, not just drift.- Provider list incomplete in CLAUDE.md. Lists 8 providers; the directory has 11 sources. Missing:
minimax.ts,nvidia.ts,fireworks.ts. - Hooks & CI not surveyed. No pre-commit hooks or GitHub Actions reviewed.
1. The 30,000-foot picture
The codebase is a pnpm monorepo organized around three load-bearing concepts:
┌─────────────────────────────────────────────────────────────┐
│ Stimulus (config: role + instructions + tools + options) │
│ ↓ │
│ Interaction (state: messages + model + runner + session) │
│ ↓ │
│ ModelRunner (execution: AI SDK + costs + rate limits) │
│ ↓ │
│ Provider (Google, OpenRouter, DeepInfra, Ollama, …) │
└─────────────────────────────────────────────────────────────┘Everything else is built on top:
- Habitat = a directory + config + sessions + tools + sub-agents, with a multi-protocol HTTP server (MCP, A2A, web chat) on top.
- Gaia = a habitat that orchestrates other habitats in Docker containers.
- Evaluation = batch / matrix runners over Stimulus × Models, plus ranking & combine.
- Sessions / Introspection = adapters that pull conversation history from Claude Code, Cursor, Pi, or habitat sessions, normalize them, digest them, and present them in a TUI.
- CLI / UI = Commander tree + React Ink TUIs + Telegram/Discord adapters wrapping
Interaction.
The dependency DAG as designed (per CLAUDE.md): core ← server ← evaluation ← habitat ← ui ← cli. The DAG as built is mostly right, with two seams: ui/index.ts re-exports habitat internals, and cli → sessions → ui is a runtime path that side-steps the documented order.
2. Subsystem-by-subsystem
2.1 Cognition (packages/core/src/cognition/) — healthy, one fat file
The cleanest subsystem. BaseModelRunner is the workhorse; Stimulus is data; Interaction is state; providers plug in via a thin BaseProvider. The HARD RULE about not capping maxTokens is actively guarded by request-options.test.ts.
Drift: runner.ts is 876 LoC with a 320-line streamText containing a provider-specific usage-extraction cascade (Ollama / OpenRouter / Google / GitHub Models) that should live in usage-extractor.ts. ModelResponse.messages? is on the TS type but missing from the Zod schema (silent shape drift waiting to bite). ModelRunner.interaction: any is a forward-ref hack. SmartModelRunner.RunnerModification is deprecated dead code. An ASCII-strip regex (runner.ts:264) silently drops non-English reasoning traces.
2.2 Interaction + Stimulus (packages/core/src/interaction/, stimulus/) — carrying a hidden feature pipeline
Interaction (382 LoC) and Stimulus are tight and well-bounded. load-interaction.ts is the right single entry point for opening a session by id. The SessionAdapter + AdapterRegistry design is textbook.
Drift: Four top-level dirs — projection/, promotion/, reflection/, knowledge/ — implement a self-contained "Exploration / knowledge-promotion" pipeline that's not in CLAUDE.md and has only two external callers (@umwelten/sessions/introspect, @umwelten/cli/knowledge). This is a feature surface masquerading as part of core. persistence/ bundles two unrelated stores under one name (Claude-Code-specific session indexing + a generic InteractionStore whose loadSession doesn't persist metadata). session-analyzer.ts and session-digester.ts co-exist; the digester wraps the analyzer; CLAUDE.md's claim that "digests are the one source" is aspirational. SessionSource is declared twice (normalized-types.ts and types/types.ts).
2.3 Habitat (packages/habitat/) — sprawling, four HTTP servers, two routing systems
The biggest concentration of drift. ~20.5K LoC. Heavy hitters: tools/gaia/gaia-tools.ts (1347), tools/agent-runner-tools.ts (1183), container-server.ts (1128).
What's clean: Habitat factory + ToolRegistry, the identity/ module, ChannelBridge as the unified chat plumbing, serve.ts as the single boot entry, and "Gaia = habitat + extra ToolSet" as a composition principle.
Drift:
- Four HTTP servers, three of which independently re-implement
parseRoute/matchRoute/serveStatic/sendJson:container-server.ts,web/server.ts,tools/gaia/routes.ts.mcp-local-server.tsduplicatesregisterAiToolverbatim from container-server.gaia-server.tsis self-labeled legacy but still live on port 7421. - Two routing systems for channel-→agent mapping: legacy
discord-routing.ts+discord-provision.ts(the latter importsdiscord.jsand arguably belongs in@umwelten/ui/discord/) vs modernbridge/routing.ts, which already reads the legacy file as fallback. - Two slash-command systems:
slash-commands.ts(CLI REPL) vsbridge/commands.ts(channel bridge), different lists, different consumers. - CLAUDE.md is wrong about
bridge/: it claimsbridge/diagnosis-agent.ts,bridge/monitor-agent.ts, andbridge_diagnose/bridge_monitortools exist. None do. The Tool Sets table mentionsbridge_*tools that are only error strings now. - Implicit cross-cutting state:
Habitat._currentSessionIdis accessed via(habitat as any)._currentSessionIdcasts.
2.4 Evaluation + Sessions + Protocols — three eval systems, one is live
Evaluation — deeper look (post second-pass). On closer inspection there are three distinct evaluation systems in the package, and only one is actively maintained:
- System A — the CLI path (
cli/eval.ts→api.ts:runEvaluation→Evaluation→EvaluationRunner→FunctionEvaluationRunner). Output layout:output/evaluations/<id>/responses/. Frozen since the monorepo split (last meaningful commit oncli/eval.tsandapi.tsis the extraction commit itself; no feature work since). Total:cli/eval.ts871 LoC,api.ts825,base.ts76,runner.ts31,evaluate.ts42,ui/EvaluationApp.tsx215 = ~2060 LoC of obsolete surface. - System B — the strategy classes (
SimpleEvaluation,MatrixEvaluation,BatchEvaluation). OnlySimpleEvaluationhas any consumer (used byEvalSuiteand the model-showdown examples).MatrixEvaluation(235 LoC) andBatchEvaluation(218 LoC) have zero source consumers — only their own tests. Last touched at extraction commit. - System C —
EvalSuite+llm-eval/. This is the live spine.llm-eval/runFullEval(model, opts)composes three sub-suites (language / coding / tool-calling), each anEvalSuite. PropagatesAbortSignaldown to the AI SDK call so a watchdog can actually cancel an HTTP request. Driven byexamples/local-providers/run-matrix.tsthrough a 2-layer harness (eviction + preflight + AbortController watchdog). Commits like9548bf9(partial-response salvage + transcript replay),93bb375(watchdog + undici timeout), and the local-providers fleet are the recent activity.
The CLI eval command is no longer the canonical eval path — the examples are. umwelten eval run/batch/report still works (it's wired) but every feature added in the past six months has gone into EvalSuite / llm-eval/ / examples/local-providers, not api.ts. The CLI path can be retired.
Verified dead code (zero source consumers):
evaluation/codebase/— 4 files, ~1620 LoC. "LLM modifies real codebases" framework that never landed.evaluation/analysis/result-analyzer.ts— 262 LoC, only its own test.evaluation/scorer.ts— 23 LoC, no subclasses.evaluation/strategies/matrix-evaluation.ts— 235 LoC.evaluation/strategies/batch-evaluation.ts— 218 LoC.evaluation/tool-testing/types.ts— keep onlyToolTestResult(used by reporter), drop the other ~180 LoC.evaluation/introspection/browse.tsshim — pure re-export of@umwelten/sessions/introspection/browse.js.ui/EvaluationUI.tsx— 183 LoC, superseded byEvaluationApp.tsx, no importers.
If you commit to phasing out the CLI eval path (which the activity log says you already have), an additional ~2200 LoC becomes deletable: api.ts, base.ts, runner.ts, evaluate.ts, types/evaluation-types.ts, cli/eval.ts, ui/EvaluationApp.tsx.
Total removable: ~4600 LoC of ~12,100 = ~38% reduction. The package distills to one coherent stack: EvalSuite + llm-eval/runFullEval + ranking/ + combine/ + Reporter, driven by scripts in examples/.
CLAUDE.md says reporting/ lives in packages/core/src/reporting/ — it doesn't; it lives in evaluation.
Sessions: A small Commander-command package. CLAUDE.md says src/introspection/ is in core — it isn't; it's here. introspection/storage.ts + types.ts still encode the old IntrospectionRun / DecisionLogEntry model that CLAUDE.md says is gone. sessions.ts is 3640 LoC of inline command bodies.
Protocols: Two MCP clients (legacy hand-rolled mcp/client/client.ts vs mcp/client/remote.ts using the official SDK), two MCP servers (legacy mcp/server/server.ts vs mcp-serve/), exported as peers. habitat/mcp-local-server.ts re-implements mcp-serve/mcp-handler.ts instead of consuming it (acknowledged TODO at container-server.ts:555). a2a/chat.ts isn't A2A protocol — it talks to habitat's /api/chat and is misplaced.
Digest persistence duplicated three ways: sessions/introspection/browse.ts (getDigestPath/saveDigest/loadDigest), core/interaction/analysis/extraction-engine.ts (persistDigest, with a comment acknowledging the duplication).
2.5 CLI + UI — dead UIs, double Telegram, business logic in CLI
CLI: 11 top-level commands, mostly clean Command-re-export pattern in cli.ts. But commonOptions.ts is used by only 4 of 11 (the others redefine their own -p/-m). eval.ts is 871 LoC mostly of input validation that belongs in evaluation. habitat.ts is 943 LoC including secrets-command logic. knowledge.ts is undocumented in CLAUDE.md.
UI: Two REPL loops coexist by design — cli/repl.ts is habitat-aware (used by umwelten habitat) and cli/CLIInterface.ts + CommandRegistry is the stateful-class non-habitat path (used by umwelten chat, one sessions subcommand, and the bare-bones example apps). Documented split, not pending drift. EvaluationUI.tsx is dead (no importers; superseded by EvaluationApp.tsx). ExploreBrowseApp.tsx is superseded by DashboardApp.tsx but still exists. DiscordAdapter.tsx was 2083 LoC of god-component, now 1641 after Wave E. ui/index.ts re-exports habitat internals, blurring the dep DAG.
Cross-cutting: Telegram has two entry points (cli/telegram.ts math demo vs cli habitat telegram habitat-aware) with overlapping intent. cli → sessions → ui is a runtime path that side-steps the documented cli → ui dependency.
2.6 Core support modules — two undocumented, one stale dir, missing exports
context/, costs/, markdown/, rate-limit/ are clean leaf utilities. schema/ is fine but has an empty stale schema-temp/ dir.
Drift:
session-record/andenv/are not in CLAUDE.md but both are real, actively-used core modules.session-record/is the storage substrate for Habitat / Telegram / Discord transcript resume and learnings (extracted from habitat to break ahabitat ↔ uicycle — the layering implication: core knows about habitat-specific filesystem conventions).env/is thedotenvside-effect import that makes API keys work — every consumer of core benefits.memory/is not exported fromindex.tsand has zero callers outside its own tests. Either promote it or move toexamples/.rate-limit/is not inindex.tsbutevaluation/suite.tsdeep-importsclearAllRateLimitStates. Pick one.memory/determine_operations.tshas leftoverconsole.logdebug output.markdown/from_html.tsexports an unusedfromHtmlViaModel.- Two
streammarkambient declarations (types.d.tsandtypes/streammark.d.ts) — pick one.
3. The drift catalog (consolidated)
Sorted by likely return-on-cleanup:
High-value cleanups (small surface, big clarity win)
- Fix CLAUDE.md's location claims. It's wrong about
reporting/(says core, is evaluation),introspection/(says core, is sessions), and thebridge/module entirely. Two real modules (session-record/,env/) are missing. Thebridge_diagnose/bridge_monitortools it advertises don't exist. - Delete dead code.
evaluation/codebase/(~50KB, no consumers),evaluation/analysis/result-analyzer.ts,evaluation/scorer.ts,ui/EvaluationUI.tsx,ui/tui/introspect/ExploreBrowseApp.tsx,schema/schema-temp/(empty),markdown/fromHtmlViaModel,test-utils/load-env.ts(no callers), the leftoverconsole.loginmemory/determine_operations.ts, the deprecatedRunnerModificationinSmartModelRunner. - De-duplicate digest persistence. Move
loadDigest/saveDigest/getDigestPathintocore/interaction/analysis/next tosession-digester.ts; re-export from@umwelten/sessions. Dropextraction-engine.ts's duplicatepersistDigest(the comment already acknowledges the duplication). - Resolve
SessionSourcedouble declaration betweennormalized-types.tsandtypes/types.ts. - Consolidate the two
streammarkambient declarations.
Medium-value (structural, but contained)
- Extract
streamText's provider-specific usage cascade fromrunner.ts:408-546intousage-extractor.ts. Drops runner LoC ~140 with no behavior change. - Collapse
web/server.tsintocontainer-server.ts(or vice versa) — extract a smallHttpAppShellso all three habitat HTTP entry points reuse one router/static/CORS/sendJson implementation. - Make
mcp-local-server.tsa mode ofcontainer-server.ts(or a thin wrapper around it) instead of a parallel implementation. Resolves thecontainer-server.ts:555TODO. - Sunset legacy MCP code in
@umwelten/protocols/mcp/{client/client.ts,server/server.ts}. The only live consumer is one debug subcommand incli/mcp.ts. - Split
gaia-tools.ts(1347 LoC) intogaia-tools/{habitats,secrets,skills,standards,index}.ts. Splitagent-runner-tools.ts(1183 LoC) one tool per file. - Pick one slash-command system in habitat:
bridge/commands.tsis the modern one. (The REPL split —repl.tshabitat-aware vsCLIInterface.tsnon-habitat — is intentional now; both files have header comments explaining the rule of thumb.) - Pick one Telegram entry point: kill
cli/telegram.ts(the math-demo) or merge it intocli habitat telegram. - Move
discord-provision.tsto@umwelten/ui/discord/— it importsdiscord.js. Deletediscord-routing.tsoncebridge/routing.tshas fully absorbed it (already reads the legacy file as fallback). - Move evaluation input validation from
cli/eval.tsinto@umwelten/evaluation. CLI should parse and dispatch, not validate domain rules. - Move
Stimulustest frominteraction/stimulus.test.ts→stimulus/(stray after a refactor).
Bigger / structural
- The Exploration / knowledge pipeline (
interaction/{projection,promotion,reflection,knowledge}/) should either be split into its own package (@umwelten/knowledgeor merged into@umwelten/sessions) or documented in CLAUDE.md as a first-class subsystem. Right now it lives anonymously inside core. Same fordomain-types.ts(Exploration / SourceSession types misplaced underinteraction/types/). - Decide analyzer vs digester. If digester is canonical, deprecate
analyzeSessionWithRetryfrom the public surface. Right now both exist and the digester wraps the analyzer. - Fix the DAG seams: stop
ui/index.tsfrom re-exporting habitat internals (startWebServer,ChannelBridge, etc.); rethink thecli → sessions → uiruntime path. session-record/layering: core knows habitat filesystem conventions. Long-term, either accept this as "core hosts the cross-cutting session substrate" (and document it) or hoistsession-recordto its own package and pull both habitat and core to depend on it.Habitat._currentSessionId— make it explicit (thread through tool contexts, the waygetSessionIdis already a callback in some tools) or rename / document.
4. The "what's actually clean" inventory
Worth preserving and emulating:
Stimulus+Interactioncore API (data / state split, single-runner delegate).BaseModelRunnerHARD RULE compliance, guarded byrequest-options.test.ts.request-options.tsandprovider-options.ts— table-driven, well-commented.SessionAdapter+AdapterRegistrypattern.load-interaction.tsas the source-agnostic entry point.stimulus/tools/agent-kit.tsfactory chain (path-sandbox → fs-tools → bash-tool → agent-kit).stimulus/skills/progressive-disclosure design.Habitat.create()8-step init +ToolRegistrylate-bound stimulus.identity/module — vault + manifest + skill-inspector + call-context.ChannelBridgeas the unified chat plumbing for every UI adapter.serve.tsas the single boot entry for habitat HTTP.- Gaia-as-habitat composition (habitat + one extra ToolSet).
a2a/server.tsanda2a/client.ts— small, sharp, no habitat coupling.mcp-serve/— the modern OAuth-backed framework with cleanUpstreamOAuthProvider/McpToolRegistrar/McpServeStoreinterfaces.EvalSuite,combine/,ranking/in evaluation.- The CLAUDE.md HARD RULES section itself — the prose plus regression tests is exemplary architectural discipline.
5. Documentation strategy
CLAUDE.md is the canonical map. Three classes of correction needed:
Reality fixes (CLAUDE.md is wrong)
reporting/is in@umwelten/evaluation, not core.introspection/is in@umwelten/sessions, not core.- The
bridge/description should listchannel-bridge.ts,commands.ts,routing.ts— notdiagnosis-agent.ts/monitor-agent.ts. - The Tool Sets table should drop
bridge_diagnose/bridge_monitor/bridge_*references. - The user memory note that "File/time/URL tools are NOT in standardToolSets" contradicts
tool-sets.ts— code is the truth.
Additions (real modules absent from CLAUDE.md)
src/session-record/— the storage substrate (transcripts, learnings, compaction events). Heavily used by habitat, Discord, Telegram, the digester.src/env/— thedotenvside-effect import.cli/knowledge.ts— the Exploration / knowledge-promotion command.- The Exploration pipeline (
interaction/{projection,promotion,reflection,knowledge}/) — if it's staying in core, document it as a first-class subsystem.
Aspirational claims to remove or align
- "Digests are the one source of session analysis" — both the analyzer and digester are live; either align the code or soften the claim.
- "There is no longer a separate introspection LLM pipeline" —
IntrospectionRun/DecisionLogEntrydata model still lives insessions/src/introspection/storage.tsand is read bybuildBrowse().
6. Cleanup progress & next passes
Done
Wave A — Evaluation package overhaul (4 commits, ~−6800 LoC):
045c89d— Delete verified dead code:evaluation/codebase/(~1620 LoC, never-landed),analysis/result-analyzer,scorer.ts,strategies/matrix-evaluation,strategies/batch-evaluation,introspection/browse.tsshim,ui/EvaluationUI.tsx.f7c6ed3— Remove obsolete CLI eval path:cli/eval.ts,evaluation/api.ts,Evaluationhierarchy,evaluation-types.ts,ui/EvaluationApp.tsx.056c2e6— Fix unresolved imports (caught by knip): inline types inranking/types.tsandstrategies/simple-evaluation.ts.d74fc26— Move digest persistence into core; break ui↔sessions cycle.
Evaluation package shrank from ~12,100 LoC to ~8K. Single coherent stack: EvalSuite + llm-eval/runFullEval + ranking/ + combine/ + Reporter, driven by scripts in examples/.
Wave B — Mechanical sweep (7 commits, −87 LoC + behavior fixes):
7bb2e53— DeletefromHtmlViaModel(orphan LLM HTML→md converter).3088ccf— Strip debugconsole.logs + unusedzodToJsonSchemafrommemory/determine_operations.ts.9a66ab7— Delete deprecatedRunnerModificationfromSmartModelRunner.32372e4— Delete orphantest-utils/load-env.ts.4dbd7cb— Drop misleading-shapestreammarkshim.dae6a28— Stop stripping non-ASCII from reasoning deltas (was silently dropping Qwen/GLM/DeepSeek/nemotron non-Latin reasoning).ab1bd8c— UnifySessionSource; dropSessionSourceForEntryduplicate.
Tests: 1183 → 1182 (the one removed test was for RunnerModification).
Wave C — Documentation sync (7 commits, CLAUDE.md only, +193 / −96 lines):
19f1ffe— Package map + DAG: drop dead@umwelten/server(renamed in 8f44b16), add@umwelten/sessions+@umwelten/protocols, flag the two DAG seams.4aad868— Retireumwelten evalCLI references; rewrite the evaluation section aroundEvalSuite+llm-eval/runFullEval.489ef79— Rewritebridge/section (thediagnosis-agent.tsandmonitor-agent.tsit claimed never existed); drop deadbridge_*tools from the Tool Sets table.e1950b5— Fix module locations:mcp/→@umwelten/protocols,introspection/→@umwelten/sessions+@umwelten/core,reporting/→@umwelten/evaluation.0afc30e— AddCONTEXT.mdpointer,src/session-record/,src/env/, the Exploration pipeline, and three missing providers (minimax,nvidia,fireworks).a744ba5— Fix CLI command list (no moreeval; addknowledge/browse/introspect), correct theInteractionusage example (no.chat()method), updateui/inventory.b87ea22— Mop-up: two leftover@umwelten/serverreferences inside the habitat section.
CLAUDE.md: 507 → 604 lines. Grep-verified no remaining references to deleted symbols.
Wave D — Cognition extractions (2 commits, runner.ts 870 → 732 LoC):
a64a5de— ExtractextractStreamUsage(response, initialUsage, provider)fromrunner.ts:402-551intousage-extractor.ts. Provider-specific cascade (Ollama / OpenRouter / MiniMax / Google / GitHub Models) now lives next tonormalizeTokenUsageandcalculateCostBreakdown. Verified with unit tests + knip + a realgemini-3-flash-previewsmoke test (--debug-usageconfirmed all five token keys flowed through cleanly).c473719— SyncModelResponseSchemawith theModelResponseTS type. Addsmessages: z.array(z.unknown()).optional()to the schema; collapses the type-side intersection so the schema is the single source of truth.
Still on the cognition wish-list (not done this pass): ModelRunner<I = Interaction> generic so interaction: any can drop the cast.
Wave D′ — Verification & the bug it uncovered (4 commits):
Wave D claimed "behavior-preserving" off the back of one Google smoke call. The hardening pass turned that into real coverage, and in doing so uncovered a silent data-corruption bug that had been live for months.
f539578— 27 unit tests for the extracted cascade pinning every provider branch with mock response shapes plus end-to-end normalize-downstream checks. Test count 1182 → 1209.fad46e1—scripts/smoke-test-cascade.ts+pnpm smoke:cascade. Drives the realBaseModelRunnerthrough every cascade provider on bothgenerateTextandstreamTextpaths and reports tokens / cost / duration per cell.
The bug: going through the smoke output cell-by-cell (not just "pass / fail") revealed that MiniMax and GitHub Models streamText had been silently returning {promptTokens: 0, completionTokens: 0} while reporting tests as green. Six months of benchmark cost data on those two providers was zeros.
Two-layer fix:
720f4c8— Defense-in-depth:normalizeTokenUsagenow returnsnullinstead of{0, 0}when every numeric field isundefined. Triggers the existing "usage not available" warning instead of silently writing zeros. Kept in place even after the root-cause fix — if a new provider regresses, the system warns loudly.921d1af— Root cause: our ninecreateOpenAICompatiblecalls didn't passincludeUsage: true. The AI SDK option exists (since@ai-sdk/openai-compatible1.0.0; we're on 1.0.29) and defaults to false, which means the SDK omitsstream_options.include_usagefrom the request body. Servers that follow the spec strictly (MiniMax, GitHub Models) honored that and didn't send the final usage SSE chunk. AppliedincludeUsage: trueto all nine providers (minimax, github-models, deepinfra, fireworks, llamabarn, llamaswap, lmstudio, nvidia, togetherai).
Final smoke matrix: 10 of 10 ok, 0 expected failures, 0 unexpected failures. All five cascade providers return non-zero token counts on both paths.
Meta-lesson: "tests passing" without integration coverage created false confidence. The vitest unit suite never traversed the extracted cascade; the smoke script does. Run pnpm smoke:cascade before any cognition-layer change ships.
Wave E — UI/habitat consolidation (3 of 4 items resolved on main):
Each item was framed as a quick "pick one of two." Two became real cleanups, one was kept-on-purpose with documentation, and the last is still pending.
- Telegram ✅
9b0010c— standaloneumwelten telegramdeleted;umwelten habitat telegramis the canonical entry.docs/guide/telegram-bot.mdrewritten as a redirect. - Channel routing ✅
ba8b3f1— legacydiscord-routing.ts(265 LoC) deleted; every bridge-vs-legacy branch in DiscordAdapter.tsx collapsed (2083 → 1641 LoC);discord-provision.tsported tobridge/routing.ts;DiscordChannelRuntimeMode→ChannelRuntimeMode. Existingdiscord.jsonfiles keep working (bridge reads them as fallback). - discord-routing-tools ✅
bdadf5a— unused 90-LoC AI SDK tool stub deleted. - REPL framework — kept as a documented split. The audit's "delete CLIInterface, only used by
umwelten chat" was wrong: CLIInterface has six callers, including thesimple-agent/bare-bones-memoryexample apps that exist specifically to demonstrate the non-habitat path.runRepl(habitat-aware) andCLIInterface(non-habitat, stateful class with pluggable command registry + stats tracking) serve different consumers. Both files now have header comments explaining the split; CLAUDE.md's@umwelten/uisection has the rule of thumb. - Slash-command system — still pending.
bridge/commands.tsvsslash-commands.ts. Needs deeper audit before scoping (same lesson as REPL).
Meta-lesson: the "pick one of two" framing was systematically optimistic about how independent the two halves of each pair were. Verify before scoping.
Wave F — MCP / shared-helper cleanup (2 of 4 audit items resolved):
d538337— Deleted@umwelten/protocols/mcp/server/server.ts(682 LoC of hand-rolled JSON-RPC) + the brokenumwelten mcp create-serverCLI stub that was its only caller.e5695ce— DeduplicatedregisterAiToolbetweencontainer-server.tsandmcp-local-server.tsinto a new sharedmcp-tool-bridge.ts(80 LoC).
Held: MCP client port (legacy MCPClient → official SDK for connect/test-tool/read-resource) needs live MCP-server testing across stdio/SSE/WS. HttpAppShell extraction needs a live habitat serve exercise — unit tests won't catch serveStatic MIME edges, CORS preflight, or SSE chat stream regressions. Both are genuine refactors, not pure deletes.
Wave G — Big-file splits (3 of 4 splits landed):
60cb484—tools/agent-runner-tools.ts(1183 LoC, 7 tools) split per-tool-per-file. Tools average ~150 LoC; per-file is the right grain.da6c041—tools/gaia/gaia-tools.ts(1347 LoC, 30 tools) split per-domain factory. Per-tool would have been wrong — tools average 33 LoC, half under 25. Domain grouping matches the conceptual structure already in the source.66b862a—packages/sessions/src/sessions.ts(3640 LoC, 23 Commander subcommands) split per-domain registrar. Factory signature isregisterXxxCommands(parent: Command): voidinstead ofcreateXxxTools(ctx)— the only thing that changed between passes.
Convention codified after three passes:
When a file exceeds ~1000 LoC and holds N independent items composed by a single entry function, split it into a sibling directory:
helpers.tsfor shared types/functions, one file per domain (or per item if items are large),index.tsto compose, and a thin re-export shim at the original path. The composition factory's signature matches the existing idiom —createXxxTools(ctx)for AI SDK tools,registerXxxCommands(parent)for Commander, etc. Granularity rule of thumb: target ~100–500 LoC per file; group below 80, split above 500. Behavior preserved verbatim.
Still pending: DiscordAdapter.tsx (1641 LoC after the Wave E reductions). Less urgent than the three completed splits because the file is now under 2000 LoC.
Next
Wave H — Structural decisions (discussion first, then 1 week+):
- Exploration / knowledge pipeline location (split out vs document as core subsystem).
- Analyzer vs digester (deprecate analyzer or soften CLAUDE.md's claim).
session-record/layering (stay in core with documentation, or hoist to own package).Habitat._currentSessionIdcast-based state (thread explicitly or rename + document).- DAG seams (
ui/index.tsre-exporting habitat;cli → sessions → uiruntime path).
Recommended next step: only structural / discussion-required items remain. In increasing order of design weight:
- MCP client port (held out of Wave F): rewrite
cli/mcp.ts'sconnect/test-tool/read-resourcesubcommands against the official@modelcontextprotocol/sdk, then deleteprotocols/mcp/client/client.ts. Bounded scope; needs live MCP-server testing. - Wave F3 —
HttpAppShellextraction: pull router/static/CORS/sendJson out ofcontainer-server.ts. Needs a realhabitat serveexercise. - Slash-command unification (held out of Wave E):
bridge/commands.tsvsslash-commands.ts. Likely sits between the Telegram case (real winner) and the REPL case (documented split). - Wave H structural decisions above — each needs a design call before code moves.
Verification habit established by Wave D′: before any cognition-layer change ships, run pnpm smoke:cascade and verify the cell-by-cell token counts are non-zero. "All tests pass" is not proof on its own — the original 27 unit tests passed against the broken streamText path. For UI/habitat changes (Wave E onward), launch the affected REPL/bot/web UI and click through the golden path; the test suite won't catch interactive regressions.