feat(team): update_teammate/4 rebinds a teammate and moves its computer - #1802
Merged
Merged
Conversation
jhgaylor
force-pushed
the
stack/1636-cotenant-identity
branch
from
September 10, 2026 11:10
7d2a630 to
b31c7cc
Compare
jhgaylor
force-pushed
the
stack/1636-update-teammate
branch
from
September 10, 2026 11:10
8298224 to
18e2f9a
Compare
jhgaylor
force-pushed
the
stack/1636-cotenant-identity
branch
from
September 10, 2026 11:26
b31c7cc to
f8ab821
Compare
jhgaylor
force-pushed
the
stack/1636-update-teammate
branch
from
September 10, 2026 11:26
18e2f9a to
7701f75
Compare
jhgaylor
force-pushed
the
stack/1636-cotenant-identity
branch
from
September 10, 2026 11:34
f8ab821 to
47a6c28
Compare
…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
force-pushed
the
stack/1636-update-teammate
branch
from
September 10, 2026 11:34
7701f75 to
67489ec
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fountain.Teamcould add a teammate and remove one. It could not move thename, environment or vault a teammate is bound to, so bulk apply (next in this
stack) had no way to reconcile a
Teammatedocument against a teammate thatalready exists.
update_teammate/4does that.attrsis string-keyed and takes the samethree keys
add_teammate/4does. An absent key leaves that binding alone; ablank 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 nothingmoved — 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'smachine out from under it: the next launch looks under the new key, builds a
fresh machine, and the old one stays
readyholding a concurrency slot and adisk carrying the old environment's secrets. This is the hazard #1084 made
Agents.update_agent/3refuse, and it is refused the same way here —{:error, :sandbox_mid_turn}before anything is written, andreset_sandbox/2on 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 isone 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 samemanifest would report
unchangedand 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/3makes and this function does not. Refusing cannot strand a teammate;
attaching wrongly can.
No API surface here — the
Teammatedocument 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):
stack/1680-unchanged-verdict— a save that changes nothing records nothing (Bulk apply reportsupdatedfor a document that changed nothing, so a client cannot tell a no-op from a write #1680)stack/1636-apply-isolation— a raise in one apply document fails that rowstack/1636-cotenant-identity— a co-tenant follows a replacement only if it declared the same identitystack/1636-update-teammate— Team.update_teammate/4 ← this onestack/1636-cli-group-by-kind— CLI: group apply documents by kindstack/1636-teammate-kind— apply reconciles Teammate documentsstack/1636-schedule-kind— apply reconciles Schedule documentsstack/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