Skip to content

feat(team): update_teammate/4 rebinds a teammate and moves its computer - #1802

Merged
jhgaylor merged 1 commit into
mainfrom
stack/1636-update-teammate
Sep 10, 2026
Merged

jhgaylor merged 1 commit into
mainfrom
stack/1636-update-teammate

Conversation

@jhgaylor

Copy link
Copy Markdown
Collaborator

Fountain.Team could add a teammate and remove one. It could not move the
name, environment or vault a teammate is bound to, so bulk apply (next in this
stack) had no way to reconcile a Teammate document against a teammate that
already exists.

update_teammate/4 does that. attrs is string-keyed and takes the same
three keys add_teammate/4 does. An absent key leaves that binding alone; a
blank value clears it, which for the two ids means the agent's own environment
and no vault. Both ids go through the agent's allowlists, as an add does.
Returns {:ok, conv, :updated}, or {:ok, conv, :unchanged} when nothing
moved — and nothing moved means nothing recorded.

The part worth reviewing is what happens to the computer. A home is keyed on
(user, agent, environment, vault), so moving either id moves the teammate's
machine out from under it: the next launch looks under the new key, builds a
fresh machine, and the old one stays ready holding a concurrency slot and a
disk carrying the old environment's secrets. This is the hazard #1084 made
Agents.update_agent/3 refuse, and it is refused the same way here —
{:error, :sandbox_mid_turn} before anything is written, and reset_sandbox/2
on the orphan once the new binding is the committed one. An ephemeral computer
is a conversation's own and is left alone; a name-only change touches nothing.

A rebinding onto an identity the agent already has a live home for is refused
with {:error, :destination_home_occupied}, and nothing is written. There is
one home per identity and the wake path builds a home rather than attaching to
one, so writing the binding anyway would leave a teammate that cannot wake at
all: the insert hits sandboxes_home_identity_index, and re-applying the same
manifest would report unchanged and not recover it.

Refusing is deliberately the conservative half of the choice. Merging the
teammate onto the machine that is already there is the other half, and it
needs the readiness, runtime-shape and quota checks attach_conversation/3
makes and this function does not. Refusing cannot strand a teammate;
attaching wrongly can.

No API surface here — the Teammate document that calls this is the next PR.

Fourth of eight PRs toward #1636. #1675 is the combined branch and stays open
as the reference until the stack is in.

Validation: mix precommit.


The stack (each PR is based on the one above it; review and merge top down):

  1. stack/1680-unchanged-verdict — a save that changes nothing records nothing (Bulk apply reports updated for a document that changed nothing, so a client cannot tell a no-op from a write #1680)
  2. stack/1636-apply-isolation — a raise in one apply document fails that row
  3. stack/1636-cotenant-identity — a co-tenant follows a replacement only if it declared the same identity
  4. stack/1636-update-teammate — Team.update_teammate/4 ← this one
  5. stack/1636-cli-group-by-kind — CLI: group apply documents by kind
  6. stack/1636-teammate-kind — apply reconciles Teammate documents
  7. stack/1636-schedule-kind — apply reconciles Schedule documents
  8. stack/1636-webhook-kind — apply reconciles Webhook documents (Bulk apply: accept Teammate, Schedule and Webhook documents so one manifest reconciles the whole estate #1636)

🤖 Generated with Claude Code

https://claude.ai/code/session_01WQyc3wWWDAeZJE1Vr6etQa

…er (#1636)

`Fountain.Team` could add a teammate and remove one. It could not move the
name, environment or vault a teammate is bound to, so bulk apply has no way to
reconcile a `Teammate` document against a teammate that already exists.

`update_teammate/4` does that. `attrs` is string-keyed and takes the same three
keys `add_teammate/4` does. An absent key leaves that binding alone; a blank
value clears it, which for the two ids means the agent's own environment and no
vault. Both ids go through the agent's allowlists, as an add does. It returns
`{:ok, conv, :updated}`, or `{:ok, conv, :unchanged}` when nothing moved — and
nothing moved means nothing recorded.

A home is keyed on `(user, agent, environment, vault)`, so moving either id
moves the teammate's machine out from under it: the next launch looks under the
new key, builds a fresh machine, and the old one stays `ready` holding a
concurrency slot and a disk carrying the old environment's secrets. This is the
hazard #1084 made `Agents.update_agent/3` refuse, and it is refused the same way
here — `{:error, :sandbox_mid_turn}` before anything is written, and
`reset_sandbox/2` on the orphan once the new binding is the committed one. An
ephemeral computer is a conversation's own and is left alone; a name-only change
touches nothing.

A rebinding onto an identity the agent already has a live home for is refused
with `{:error, :destination_home_occupied}`, and nothing is written. There is one
home per identity and the wake path builds a home rather than attaching to one,
so writing the binding anyway would leave a teammate that cannot wake at all:
the insert hits `sandboxes_home_identity_index`, and re-applying the same
manifest would report `unchanged` and not recover it.

Refusing is deliberately the conservative half of the choice. Merging the
teammate onto the machine that is already there needs the readiness,
runtime-shape and quota checks `attach_conversation/3` makes and this function
does not. Refusing cannot strand a teammate; attaching wrongly can.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQyc3wWWDAeZJE1Vr6etQa
@jhgaylor
jhgaylor force-pushed the stack/1636-update-teammate branch from 7701f75 to 67489ec Compare September 10, 2026 11:34
@jhgaylor
jhgaylor changed the base branch from stack/1636-cotenant-identity to main September 10, 2026 11:34
@jhgaylor
jhgaylor merged commit d56464c into main Sep 10, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant