ARCHITECTURE.md at the repo root is canonical for system structure —
what the router, adapters, classifier, delegate, measurement, GUI, and serve endpoint are, and why
each is shaped that way. Read it first; this document does not restate it.
What follows is the part a component description does not cover: TangleBrain runs as several processes, one of which it does not own, with state shared through the filesystem and correlation carried through an environment variable across a boundary it does not control. Those are the parts that break in ways the component view will not predict.
Four process shapes at runtime:
- The entry process —
tanglebrain,tanglebrain-gui, ortanglebrain-serve. Operator-launched. Owns routing, measurement, and the request. - CLI adapter children — authenticated third-party CLIs spawned as subprocesses, without a shell. Short-lived, one per call.
- The delegate MCP server —
tanglebrain-delegate, over stdio. Spawned and owned by the orchestrator, not by TangleBrain. This is the important one: its lifecycle, environment, and restart behavior belong to a process TangleBrain does not control and cannot introspect. - Worker threads —
delegate_many'sThreadPoolExecutorinside whichever process is serving. Not separate processes, but the only real concurrency the product creates.
The topology is a tree, not a mesh. Nothing calls back upward, and delegate targets are built as
leaves (inject_delegate=False) so a sub-task can never spawn its own sub-tasks. That single flag
is what bounds fan-out; without it, a model could drive unbounded recursive delegation with no depth
limit and no cost ceiling.
| Channel | Between | Carries | Failure mode |
|---|---|---|---|
| HTTP | entry process → openai-compat / api backends | prompt, completion | AdapterError → failover |
| stdio (MCP) | orchestrator → delegate server | tool calls | orchestrator's to handle; TangleBrain sees a dead pipe |
| subprocess argv + stdout | entry process → CLI backends | prompt, parsed output | AdapterError → failover |
| environment variable | entry process → orchestrator → delegate child | TANGLEBRAIN_TASK_ID |
silent degradation to unlinked |
| filesystem | all processes | rotation cursor, usage log | best-effort; never breaks routing |
TANGLEBRAIN_TASK_ID is minted by the CLI, injected into the orchestrator's environment, forwarded
by the orchestrator to the delegate child, and read back by run_delegate to stamp
parent_task_id. The middle hop is performed by software TangleBrain does not own and cannot test
hermetically — it is verified live against one orchestrator (Claude Code), which is honest but is
not a guarantee.
The design response is the right one: a delegation that loses the variable is recorded unlinked
rather than raising. The consequence worth holding in mind is that this failure is invisible. A
different orchestrator that does not forward environment to its MCP children produces a complete,
correct-looking usage log in which every delegation is silently unparented, and nothing anywhere
reports that linkage was lost.
If parent-task attribution ever becomes load-bearing rather than informational, this needs a positive signal — not more error handling. Tracked in #100.
- One request is single-threaded end to end. The router, classifier, and adapters are plain synchronous code.
delegate_manyis the only fan-out. Synchronous I/O-bound calls on aThreadPoolExecutor— the workload is network-wait, so threads are the right primitive and no async runtime is warranted.- Bounded by
_effective_concurrency: the operator'ssettings.delegate_max_concurrencyif set, else anos.cpu_count()-derived default. A per-callmax_concurrencymay lower but never raise it. The "never raise" direction is the safety property — a model calling the tool cannot talk the system into more parallelism than the operator allowed. - Results are returned in input order with per-item
status, so concurrency is not observable in the result contract. A failing item never sinks the batch. - Shared mutable state across threads is exactly one thing: the usage log, serialized by
measurement._LOG_LOCK.
Fully specified in data-model.md — see its "Persistence boundaries" table, which
is canonical. The one-line summary: nothing in-flight is durable, both mutable state files live
in the cache tier, and one of them (usage.jsonl) holds irreplaceable history that the cache tier
does not promise to keep.
The system degrades along a deliberate ladder rather than failing:
- Classifier error or ambiguity → route to frontier (never trap a hard task on the local tier).
- Adapter error → next orchestrator in rotation.
- All orchestrators failed and the paid gate is on → paid tier, in roster order, as a genuine last resort. A paid success does not advance the rotation cursor.
- All paths failed →
RouterErrorlisting every failure, rate-limits annotated. - Measurement failure at any point → swallowed; the answer still returns.
Step 5 is the codebase's one deliberately broad exception handler (measurement.py:376-378) and it
is load-bearing: the alternative is a logging bug that eats a successful, already-paid-for answer.
Per-failure verification is tabulated in
nonfunctional-requirements.md.
The topology assumptions worth re-checking when the system changes:
- If delegation ever recurses, the fan-out bound disappears and the cost ceiling with it.
inject_delegate=Falseis the guard; treat any change to it as a security-surface change. - If a second writer of the roster appears (a second GUI, a config API), the atomic-write-plus-backup story becomes a concurrent-write story it was not designed for.
- If anything binds off-loopback, the whole authorization model
(
security-model.md) is void, not weakened.