Skip to content

fix(bridge): child profile without a provider is routed to the harness default, not the seam's provider #740

Description

@drewstone

What happens

bridgeCellModel builds a child's wire model from the child's own AgentProfile. When that profile names a model without a provider, the seam's provider is not inherited — the harness name is prefixed directly and the child is routed to the harness's own default provider.

dist/supervise-*.js (0.128.0, unchanged since at least 0.120.0):

const model = hasBackendModel ? concreteModelId(backendModel) : qualifyProviderModel(profile.model);
...
if (model) return model.startsWith(`${harness}/`) ? model : `${harness}/${model}`;

qualifyProviderModel({ default: 'glm-5.2' }) returns 'glm-5.2', so the wire id becomes pi/glm-5.2 — no provider segment. pi then resolves glm-5.2 against its default provider.

Why it matters

The seam was configured with pi/tangle-router/glm-5.2. The child asked for the same model by name and silently got a different provider, then died before its first turn:

bridgeExecutor: bridge stream error: pi exit 1:
No API key found for amazon-bedrock.

Three children in one supervised run settled down with spent.tokens = 0. The run reported no-winner/aborted with no indication that the cause was a dropped provider segment. From the operator's side this looks like a credential problem, and the natural fix (add a bedrock key) is exactly wrong.

The failure is quiet in the way that matters: a provider-less model is a valid wire id, so nothing rejects it. It routes somewhere real, just not where the seam pointed.

Why the obvious workarounds don't apply

  • Seam agentProfile overlay — correctly refused: supervise: backend agentProfile overlays are not allowed because they run after spawn authorization. A child could be authorized as one profile and executed as another, so this is not a gap to reopen.
  • allowedModelsassertProfileModelsAllowed checks profile.model?.default, .small, subagent and mode models. All of those are bare names (glm-5.2); none carries the provider. The allow-list cannot distinguish the correct route from the broken one.
  • Instructing the supervisor to always write the provider — tried; a run still lost three children to it. Prompt compliance is not a sound guard for a wire-level default.

Proposed fix

When the profile's model has no provider, and the seam model resolves to the same harness, inherit the seam's provider segment rather than emitting a provider-less wire id:

// seam: pi/tangle-router/glm-5.2 ; profile: { default: 'glm-5.2' }
// currently -> pi/glm-5.2        (harness default provider; wrong)
// proposed  -> pi/tangle-router/glm-5.2

If inheriting is considered too implicit, the safe alternative is to reject a provider-less profile model when the seam carries a provider, so the failure names its own cause at spawn authorization instead of surfacing as a missing credential for a provider nobody configured.

Either way the invariant worth stating is: a child that names the seam's model must not be routed to a different provider than the seam.

Reproduce

Supervise with a bridge backend on pi/<provider>/<model>, and have the supervisor spawn a child whose profile is { harness: 'pi', model: { default: '<model>' } } — provider omitted. The child settles down, spent.tokens = 0, with a credential error naming the harness's default provider.

Observed with runtime 0.128.0, cli-bridge provider 0.3.5, pi (earendil) 0.83.0.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions