Skip to content

fix(conversations): a co-tenant follows a replacement only if it declared the same identity - #1801

Merged
jhgaylor merged 1 commit into
mainfrom
stack/1636-cotenant-identity
Sep 10, 2026
Merged

fix(conversations): a co-tenant follows a replacement only if it declared the same identity#1801
jhgaylor merged 1 commit into
mainfrom
stack/1636-cotenant-identity

Conversation

@jhgaylor

Copy link
Copy Markdown
Collaborator

When a sandbox is gone and a conversation wakes onto a replacement,
move_cotenants/3 repointed every live conversation that shared the old
machine at the new one. It did that without looking at what those
conversations declared. The replacement was built from the waking
conversation's environment and vault, so a co-tenant that named a different
pair ended up running on another conversation's environment files and vault
material, while its own row said something else. Which binding won depended
on which conversation happened to wake first.

Co-tenants normally share one identity, because attaching to a machine
requires the same agent, environment and vault. They can diverge afterwards,
and the next PR in this stack is what makes them diverge: rebinding a teammate
moves one conversation's environment or vault and leaves its co-tenants
where they were. This is the invariant that rebinding needs, landed on its
own so the change to the wake path can be read by itself.

_unsafe_list_cotenants_with_identity/2 reads each co-tenant's effective
pair — the conversation's own override, falling back to its agent's
environment — which is what a sandbox row carries and what
_unsafe_find_home/4 keys on. Only the co-tenants that match the replacement
follow onto it. One that declared something else keeps pointing at the retired
row, which its own next wake reads as :create_new and builds from its own
pair.

Every co-tenant is still told the disk is gone, because for every one of them
it is. The ones left behind are told why they did not follow.

Third 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 three new cases were checked against the old
move_cotenants/3 and fail there.


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 ← this one
  4. stack/1636-update-teammate — Team.update_teammate/4
  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

…ared the same identity (#1636)

When a sandbox is gone and a conversation wakes onto a replacement,
`move_cotenants/3` repointed every live conversation that shared the old
machine at the new one, without looking at what those conversations declared.
The replacement was built from the *waking* conversation's environment and
vault, so a co-tenant that named a different pair ran on another conversation's
environment files and vault material while its own row said something else.
Which binding won depended on which conversation happened to wake first.

Co-tenants normally share one identity, because attaching to a machine requires
the same agent, environment and vault. They can diverge afterwards, and the
next change in this series is what makes them diverge: rebinding a teammate
moves one conversation's environment or vault and leaves its co-tenants where
they were. This is the invariant that rebinding needs, landed on its own.

`_unsafe_list_cotenants_with_identity/2` reads each co-tenant's effective pair
— the conversation's own override, falling back to its agent's environment —
which is what a sandbox row carries and what `_unsafe_find_home/4` keys on.
Only the co-tenants that match the replacement follow onto it. One that
declared something else keeps pointing at the retired row, which its own next
wake reads as `:create_new` and builds from its own pair.

Every co-tenant is still told the disk is gone, because for every one of them
it is. The ones left behind are told why they did not follow.

The three new cases were checked against the old `move_cotenants/3` and fail
there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQyc3wWWDAeZJE1Vr6etQa
@jhgaylor
jhgaylor force-pushed the stack/1636-cotenant-identity branch from f8ab821 to 47a6c28 Compare September 10, 2026 11:34
@jhgaylor
jhgaylor changed the base branch from stack/1636-apply-isolation to main September 10, 2026 11:34
@jhgaylor
jhgaylor merged commit e0f85ab 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