ares-server 0.10.1

ARES agent server with multi-provider LLM support, tool calling, RAG, and MCP integration
Documentation
# Fiber lifecycle

A fiber is the lifecycle owner of one registration. Its state machine has seven states. The type is `cordis::fiber::FiberState`.

## States

| State | Meaning |
| --- | --- |
| `Inactive { error }` | Not serving. Pristine, waiting for a first apply, or resting after an unsatisfied declaration. |
| `Loading` | A plugin activation is in flight. |
| `Active { epoch }` | Serving. `epoch` encodes every declared dependency version. |
| `Reloading` | A refresh pass runs. Effects may be undone and re-applied. |
| `Unloading { error }` | Effects are being disposed in reverse order (LIFO). |
| `Pending` | Reactive waiting. Effects were disposed, but the fiber is not disposed. It waits for dependencies or its readiness gate. |
| `Failed { error }` | Terminal after an apply error, or inspectable after an availability-predicate rejection. |

`Pending` carries no error. Reactive waiting is not a failure.

## State diagram

```mermaid
stateDiagram-v2
    [*] --> Inactive
    Inactive --> Loading : dependencies satisfied and gate open
    Loading --> Active : runner returns Ok(true)
    Loading --> Inactive : runner returns Ok(false)
    Loading --> Failed : runner returns Err
    Active --> Reloading : dependency epoch changed
    Active --> Unloading : reactive dependency loss or closed gate
    Reloading --> Unloading : effects undone before re-apply
    Unloading --> Pending : fiber ever activated
    Unloading --> Inactive : dispose completes
    Pending --> Loading : gate opens or provider returns
    Pending --> Pending : still-closed gate (no-op)
    Failed --> Failed : terminal; refreshes refuse to revive
```

Observers see the exact documented sequence across one reactive cycle: `Active`, `Reloading`, `Unloading`, `Pending`, `Loading`, `Active`. Subscribe with `Fiber::subscribe_state`. Test anchor: `observer_sees_unloading_pending_active_sequence`.

## One fiber's life, narrated

Follow one registration through its whole life. Every step names the
region inside `Fiber::refresh` (`crates/cordis/src/fiber.rs`) that owns
it.

**1. Registration.** `RegistryService::register` runs the factory once,
records the raw config on the fiber, and rests it `Inactive` until its
dependencies exist. No undo exists yet.

**2. Dependencies appear.** A provider registers elsewhere. The kernel
re-kicks our fiber. `refresh` acquires the transition lease, then checks
the terminal short-circuit region: no apply error is recorded, so the
pass continues.

**3. Activation.** The pass reaches the runner region. The runner re-runs
and pushes effects — timers, listeners, provided values — onto the undo
accumulator. `Ok(true)` stores the new epoch string and rests the fiber
`Active`. Observers saw `Inactive`, `Loading`, `Active`.

**4. Provider withdrawal.** Another module disposes the dependency's
provider. A refresh computes a new epoch that differs from the stored
one. The genuine-loss guard fires: the provider reads as unavailable with
version 0, the fiber has a reload runner, and it activated at least once.

**5. Unloading.** The reactive-loss region sets `Unloading { error: None }`
and pops every effect in LIFO order. Timers cancel. Listeners detach.
The provided value leaves the store. This is why consumers never observe
a half-torn configuration.

**6. Pending.** Effects are gone but the fiber survives. It rests
`Pending` with no error field. Registry pruning skips it. Strict `get`
refuses its value because none exists; nothing lies about availability.

**7. Return.** The provider re-registers. ReflectService fans the change
out and the fiber re-kicks. The Pending fast-path sees an open path this
time and falls through to one full pass. The runner region executes
again under `Loading`, effects are rebuilt, and a new epoch lands.

**8. Reactivated.** The fiber serves again with fresh effects. Consumers
that waited on strict `get` now resolve it.

Two details matter in production:

- If the plugin factory had errored during any pass, region 3 would have
  set the terminal marker instead. Region 2's short-circuit then returns
  forever. Recovery means explicit re-registration with a fresh fiber id.
- A constraint refusal over a live provider never reaches region 4. The
  provider still reports available, so the fiber rests `Inactive`, not
  `Pending`. See the version rules below.

Test anchors covering this walk: `dependent_reactivates_when_provider_returns`,
`fiber_refresh_passes_through_reloading`, `observer_sees_unloading_pending_active_sequence`.

## Reactive Pending reversal rules

The kernel converts a working configuration into `Pending` only under narrow conditions:

- The fiber lost a dependency **genuinely**: the provider is unavailable and reads provider version 0 again after its undo ran.
- The fiber has a reload runner and completed at least one fully-satisfied application (`ever_activated`). Fibers that never activated rest `Inactive` instead.
- A peer-version constraint refusal over a live provider is policy, not loss. The provider is still available, so those fibers stay `Inactive`.

Apply errors are different. A factory error sets a terminal marker (`apply_failed`). Refreshes never revive such a fiber. Recovery means explicit re-registration with a fresh fiber id. Test anchors: `dependent_reactivates_when_provider_returns`, `failed_stays_failed_on_dep_return`.

Late inject declarations never rest `Pending`. An unsatisfied declaration deactivates the fiber immediately with the note "missing or inactive dependency". The next reactive refresh converts a genuine loss to `Pending`.

A re-kick on a `Pending` fiber with a still-closed gate is a no-op. An open gate falls through to one full pass: `Pending`, `Loading`, `Active`. `RegistryService::prune_disposed` keeps `Pending` fibers alive so they can reactivate. Tests: `pending_fiber_survives_prune_disposed`, `prune_disposed_drops_disposed_but_keeps_failed`.

Every transition acquires the fiber inertia guard through a bounded wait (`TRANSITION_WAIT`, 10 seconds). Same-thread reentrancy fails immediately. Cross-task contention times out with an error naming the stuck fiber id. Tests: `reentrant_transition_detected_fast`, `contention_times_out_named`.

## Peer-version compatibility

A provider can publish a version, and an inject can demand one. The
scheme lives in `Context::VERSION_MAJOR_SCALE`
(`crates/cordis/src/context.rs`). One scale constant splits a plain `u64`
into two fields:

$$S = 100\,000, \qquad \operatorname{major}(v) = \left\lfloor \frac{v}{S} \right\rfloor, \qquad \operatorname{floor}(v) = v - S \cdot \operatorname{major}(v)$$

An inject constrained with requirement \\(r = M \cdot S + f\\) accepts a
provider of version \\(p\\) if and only if:

$$\text{satisfied}(p, r) \iff \bigl(\operatorname{major}(p) = \operatorname{major}(r)\bigr) \;\wedge\; (p \ge r)$$

Read the two clauses as separate guarantees:

- Same major means compatible. Peer dependencies never bind across a
  breaking boundary. A provider at major \\(M+1\\) leaves the dependent
  unsatisfied even when its remainder is huge.
- At or above the floor means feature-complete. Under equal majors,
  \\(p \ge r\\) reduces to \\(\operatorname{floor}(p) \ge f\\). The provider has
  every capability the consumer asked for.

Any mismatch keeps the inject unsatisfied. The dependent fiber rests
`Inactive` rather than silently binding a wrong version. This is the
"constraint refusal over a live provider" rule from the section above.

Legacy `provide` installs version 0. Version 0 satisfies only
unconstrained injects. Migrate providers to `provide_versioned` to opt
into matching. Declare the constraint side with
`Fiber::declare_inject_versioned`.

Worked example: a provider publishes \\(p = 200\,003\\) (major 2, floor 3).

| Requirement \\(r\\) | major match | \\(p \ge r\\) | Verdict |
| --- | --- | --- | --- |
| \\(2 \cdot S + 3 = 200\,003\\) | yes | yes | Satisfied |
| \\(2 \cdot S + 7 = 200\,007\\) | yes | no | Unsatisfied; floor too high |
| \\(1 \cdot S + 1 = 100\,001\\) | no | yes | Unsatisfied; cross-major |
| unconstrained | n/a | n/a | Satisfied |

Source: `VERSION_MAJOR_SCALE` documentation in `context.rs`; constraint
matching in `cordis::fiber`. Test anchor:
`refresh_reruns_provider_after_dependency_version_change` in `registry.rs`.

## Readiness barriers versus availability predicates

The two mechanisms answer different questions.

**Readiness barrier** — "not yet". Install one with `RegistryService::register_with_readiness` and compose predicates with `ReadinessBarrier::new`, `ReadinessBarrier::and`, and `with_readiness`. While the predicate reports false:

- The fiber rests in inspectable `Pending`. This is quiet, reversible waiting.
- The factory still runs once at registration, so config errors surface early.
- Strict `ctx.get` refuses values owned by non-`Active` fibers, so consumers never see the half-ready service.
- Declare watch keys with `ReadinessBarrier::watching`. Any settlement on those types fans out through `ReflectService` and re-kicks the gated fiber.

Test anchors: `ready_when_holds_pending_until_true_then_activates`, `readiness_composes_and_semantics`, `external_rekick_reactivates_waiting_fiber`.

**Availability predicate** — "never, as built". Implement `Service::check`. The registry consults it before the value reaches consumers. A rejection rests the fiber as `Failed { error: "availability predicate rejected service" }`. Registration itself stays non-throwing; later refreshes converge once the instance reports healthy.

Test anchors: `availability_predicate_rejection_registers_failed`, `predicate_passing_reregistration_activates_dependents`.

In short: a closed readiness gate parks the fiber quietly as reversible `Pending`. An availability rejection fails loudly to `Failed{error}`. They are complements, not alternatives.

## Two-phase staged reload

The loader applies a desired entry tree against the current tree in two phases:

1. **Verify without mutating.** Phase one resolves entries and pre-flights config trials against scratch candidates. On the first failed verification, the batch aborts. Nothing changed, so no rollback is needed.
2. **Apply in dependency order.** Phase two applies verified candidates. Begin and rebuild steps run first, then updates, then retire steps. On a failure during phase two, the loader rolls back every applied change newest-first: configs restore, rebuilt fibers dispose. The live tree serves the originals.

On any failure the loader leaves `current` unchanged, so a retry re-diffs cleanly. Config-only patches on active fibers go through the update path with a pre-flight trial, so factory apply counts stay flat.

### Rollback scenarios

The table lists what each phase-two failure undoes. "Applied" means the
step already ran when the failure happened; later steps never start.

| Failure during | Rollback action | Live-tree result |
| --- | --- | --- |
| A config update step | Restore that fiber's prior effective config from the staged candidate | Old config keeps serving |
| A rebuild (dispose + re-apply) step | Dispose the rebuilt fiber's effects in LIFO order | Original fiber's value is gone only if it was retired earlier in the batch — retire steps run after rebuilds |
| A begin step | Nothing to undo; batch stops before any mutation of existing fibers | Tree untouched |
| A retire step | Re-apply is not attempted; earlier applied updates and rebuilds roll back newest-first | Originals serve again |
| Verification (phase one) | No rollback needed; nothing mutated | Identical tree |

Newest-first ordering matters. Suppose a batch updates `A`, then rebuilds
`B` which injects `A`. If `B`'s re-apply fails, rollback disposes `B`'
first and restores `A` second — the reverse of application order.
Consumers of `A` see its original value throughout.

Test anchors: `staged_batch_rolls_back_on_first_failure`, `staged_batch_applies_in_order_on_success`.

## Cascade batching

When a provider fiber sits mid-config-update, dependent refreshes defer instead of churning. `Loader::cascade_defer_needed` probes an in-flight ledger. A deferred dependent rests `Pending` quietly. One post-settle re-kick converges every deferred dependent at once, instead of running one full cascade wave per concurrent patch. Without a `RegistryService` on the context, the probe always reports false and legacy behavior holds.

The timeline below shows two dependents deferring behind one provider
patch and converging together:

```mermaid
sequenceDiagram
    participant L as Loader
    participant P as Provider fiber (db)
    participant D1 as Dependent (api)
    participant D2 as Dependent (worker)
    L->>P: mark in-flight, stage new config
    L->>P: apply config update
    D1->>P: refresh() re-kick
    P-->>D1: cascade_defer_needed = true
    D1->>D1: Unloading -> undo effects -> rest Pending
    D2->>P: refresh() re-kick
    P-->>D2: cascade_defer_needed = true
    D2->>D2: Unloading -> undo effects -> rest Pending
    L->>L: settle batch, clear in-flight marker
    L->>D1: single post-settle re-kick
    L->>D2: single post-settle re-kick
    D1->>D1: Pending -> Loading -> Active
    D2->>D2: Pending -> Loading -> Active
```

Without batching, each concurrent patch would trigger one full cascade
wave through every dependent. With three patches landing together, that
is three teardown-rebuild cycles per dependent. Batching collapses them
into one.