Skip to content
Open
Show file tree
Hide file tree
Changes from 69 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
39fd32b
Add workspace-sync blueprint plan (E2E-encrypted cross-device workspa…
joshalbrecht Jul 13, 2026
b1d3ad0
Add secret_wrapping envelope-encryption helpers to imbue_common
joshalbrecht Jul 13, 2026
e596994
Add workspace-sync tables and /sync endpoints to remote_service_conne…
joshalbrecht Jul 13, 2026
8275b06
Add workspace-sync transport to mngr_imbue_cloud (client methods + sy…
joshalbrecht Jul 13, 2026
8657969
Add DELETE /sync/records/{host_id} (disassociation removes the row ou…
joshalbrecht Jul 13, 2026
130792d
Add minds dek_store and workspace_record_store (sync engine core)
joshalbrecht Jul 13, 2026
e640f01
Rework minds account/password machinery onto workspace records + DEK
joshalbrecht Jul 13, 2026
c55c0e9
Add minds sync scheduler, remote-workspace UI, unlock flow, and remot…
joshalbrecht Jul 13, 2026
eb77fd1
Add two-device sync integration tests + unlock/remove endpoint tests
joshalbrecht Jul 13, 2026
c55f564
Move workspace-sync spec into specs/; add changelog entries for all t…
joshalbrecht Jul 13, 2026
ff21f21
Fix stragglers: single-key repo init actually applied, ty type errors…
joshalbrecht Jul 13, 2026
5a8ebbd
Mark thread_cleanup hub-count test flaky under xdist
joshalbrecht Jul 13, 2026
d0e3b9e
Clear the sync kick event before each pass so mid-pass kicks are not …
joshalbrecht Jul 13, 2026
bf91969
Correct the sync-scheduler docstring's crash-safety claim
joshalbrecht Jul 13, 2026
0c285a0
Stop reconcile revision churn for accounts without a master password
joshalbrecht Jul 13, 2026
b291621
Accept empty provider_kind on workspace-record pushes
joshalbrecht Jul 13, 2026
6d68b90
Refuse master-password changes for accounts locked on this device
joshalbrecht Jul 13, 2026
9b10e8a
Carry a pre-hash install's plaintext master password through conversion
joshalbrecht Jul 13, 2026
5c3fa21
Never tombstone create-seed records discovery has not enriched yet
joshalbrecht Jul 13, 2026
a0c51fa
Stop locked-device pushes from scrubbing synced secrets server-side
joshalbrecht Jul 13, 2026
a362156
Fix a vacuous wrong-email assertion in the metadata-only sync test
joshalbrecht Jul 13, 2026
4bedb93
Reuse the existing mock-httpx helper for the sync client tests
joshalbrecht Jul 13, 2026
43255ed
Align MalformedCiphertextError docstring with the actual guard
joshalbrecht Jul 13, 2026
96d035c
Cover the sync CLI's --input-file payload branch
joshalbrecht Jul 13, 2026
ecaaf64
Restore @pure to render_landing_page
joshalbrecht Jul 13, 2026
dcf1d73
Pass the unlock banner's margin class through Notice's class attr
joshalbrecht Jul 13, 2026
f31442d
Log when a destroyed workspace's tombstone is skipped
joshalbrecht Jul 13, 2026
74499fb
Update on_created docstring for the (AgentId, HostId) callback
joshalbrecht Jul 13, 2026
c94db26
Scope push_all_secrets to pending locally-discovered records
joshalbrecht Jul 13, 2026
e278030
Retire the legacy associations file only once every entry is accounte…
joshalbrecht Jul 13, 2026
f5ea5cb
Parse the sync-conflict error body robustly from stderr
joshalbrecht Jul 13, 2026
7237547
Never tombstone hosted rows when this install has no device id
joshalbrecht Jul 13, 2026
fcb7dcb
Report a concurrent-insert PK race as a CAS conflict, not an agent co…
joshalbrecht Jul 13, 2026
83e6193
Surface a rowless record write as a server error, not a bogus 409
joshalbrecht Jul 13, 2026
18c8fa2
Rename the key-bundle size cap to cover both of its uses
joshalbrecht Jul 13, 2026
f2582f1
Warn when the conflict-body scan skips a brace-prefixed non-JSON line
joshalbrecht Jul 13, 2026
d0271a9
Raise a typed error from unwrap_bundle_json for malformed bundles
joshalbrecht Jul 13, 2026
33eae42
Catch SecretWrappingError in the sync scheduler loop
joshalbrecht Jul 13, 2026
2a0da5c
Correct the on_created ordering in the start_creation docstring
joshalbrecht Jul 13, 2026
c06a5bb
Wait for the first discovery snapshot before showing remote tiles
joshalbrecht Jul 13, 2026
332ab29
Convert sessions.json-era associations in the legacy migration
joshalbrecht Jul 13, 2026
a6c4efb
Align the workspace-sync design record with the shipped behavior
joshalbrecht Jul 13, 2026
ea4e1a0
Mention the sessions.json-era conversion in the minds changelog entry
joshalbrecht Jul 13, 2026
5cb8366
Cover PostgresSyncStore with unit tests against the in-memory SQL fake
joshalbrecht Jul 13, 2026
c6c385f
Drop the backup_kind field from workspace sync records
joshalbrecht Jul 13, 2026
65d72f7
Raise a typed error when the on-disk DEK file is corrupt
joshalbrecht Jul 13, 2026
43e32e1
Fix stale docstrings after the single-key backup provisioning change
joshalbrecht Jul 13, 2026
4feb6f0
Add Electron-driven workspace-sync e2e release tests
joshalbrecht Jul 13, 2026
becff44
Ensure host-side restic for the amnesia sync e2e test
joshalbrecht Jul 13, 2026
ace5805
Keep the sync e2e env-config download resilient to earlier step failures
joshalbrecht Jul 13, 2026
b70fc78
Share the snapshot sandbox dockerd bring-up with the sync e2e tests
joshalbrecht Jul 13, 2026
7c38d59
Give every minds snapshot test its own sandbox again
joshalbrecht Jul 13, 2026
fefeb2e
Gate the e2e sign-in on the session, not on a URL change
joshalbrecht Jul 13, 2026
f389ce9
Tolerate mid-probe navigations in the sync e2e polling loops
joshalbrecht Jul 13, 2026
ba8c527
Upload the key bundle the legacy conversion wraps; harden the e2e wipe
joshalbrecht Jul 13, 2026
029c577
Click through the first-run consent screen in the sync e2e landing waits
joshalbrecht Jul 13, 2026
34fc181
Let the landing backup-status fetch settle instead of reload-starving it
joshalbrecht Jul 13, 2026
1bc1cdb
Make the amnesia badge timeout self-diagnosing
joshalbrecht Jul 13, 2026
517e1b1
Retry transient connector errors in the sync e2e wait loops
joshalbrecht Jul 13, 2026
5f565e3
Give the amnesia e2e test the time its real work needs
joshalbrecht Jul 13, 2026
8e12255
Retry the flaky Vault reads in the minds snapshot job
joshalbrecht Jul 13, 2026
eb293d2
Wait out a full backups-status fetch cycle before judging the badge
joshalbrecht Jul 13, 2026
16910fb
Verify the exported backup zip instead of hanging on an Electron down…
joshalbrecht Jul 14, 2026
5f1823d
Remove the local debug flags that leaked into the offload snapshot co…
joshalbrecht Jul 14, 2026
f1cd85c
Attach stdin when writing the e2e restore sentinel
joshalbrecht Jul 14, 2026
ae1bbc7
Trigger PR checks on the verified sync e2e HEAD
joshalbrecht Jul 14, 2026
70ff7c1
Record the key-bundle heal in the minds changelog
joshalbrecht Jul 14, 2026
9da9e3f
Merge remote-tracking branch 'origin/main' into mngr/account-association
joshalbrecht Jul 14, 2026
8387035
Sweep the R2 buckets imbue-cloud backups leak in CI
joshalbrecht Jul 14, 2026
79318f9
Re-derive the sync e2e timeouts from what the tests actually take
joshalbrecht Jul 14, 2026
bf191fc
Give the sync payload caps room to grow
joshalbrecht Jul 14, 2026
bcc3b5d
Pin the create form's docker runtime in the sync e2e tests
joshalbrecht Jul 14, 2026
29cced0
Keep the retuned-timeout comments off the trailing-comment ratchet
joshalbrecht Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 77 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,21 @@ jobs:
# The Anthropic key (for live checks) is always needed; masked in the logs
# and forwarded into the offload sandbox by the just recipe below. Read via
# the minds-scoped minds_ci_test_gh role.
# HCP Vault's performance replicas intermittently 412 ("required index
# state not present"); one spaced retry absorbs the burst.
- name: Fetch Anthropic key from Vault
id: anthropic_key_fetch
continue-on-error: true
uses: imbue-ai/use-vault-secrets@main
with:
role: minds_ci_test_gh
secrets: |
minds/ci/litellm/ANTHROPIC_API_KEY
- name: Pause before the Vault retry
if: ${{ steps.anthropic_key_fetch.outcome == 'failure' }}
run: sleep 15
- name: Fetch Anthropic key from Vault (retry)
if: ${{ steps.anthropic_key_fetch.outcome == 'failure' }}
uses: imbue-ai/use-vault-secrets@main
with:
role: minds_ci_test_gh
Expand Down Expand Up @@ -807,6 +821,67 @@ jobs:
cargo install offload@0.9.10 --force
fi

# -- Sync e2e env plumbing (opt-in, same gate as build-minds-ci-env) --
# The workspace-sync e2e tests (apps/minds/test_sync_e2e.py) run INSIDE
# the offload sandbox but against the per-run ci env's real connector.
# On opt-in runs, resolve the env's coordinates from the env-config
# artifact and its SuperTokens admin secrets from Vault, and export them
# as MINDS_SYNC_E2E_* so the just recipe forwards them into the sandbox
# (the tests skip when the vars are absent). Secret values are masked by
# the use-vault-secrets action even though offload --trace echoes args.
# `!cancelled()` keeps the artifact available to the later
# minds_services step even when an unrelated earlier step failed
# (matching that step's own `!cancelled()` gate).
- name: Download deployment env config for the sync e2e tests
if: ${{ !cancelled() && github.event_name == 'workflow_dispatch' && inputs.run_minds_release_tests }}
uses: actions/download-artifact@v7
with:
name: minds-ci-env-config
path: .minds
- name: Resolve sync e2e env coordinates
if: ${{ github.event_name == 'workflow_dispatch' && inputs.run_minds_release_tests }}
id: sync_e2e_env
run: |
set -euo pipefail
CONFIG=.minds/iterate-default.json
{
echo "env_name=$(jq -er '.shared_envs.default.env_name' "$CONFIG")"
echo "connector_url=$(jq -er '.shared_envs.default.connector_url' "$CONFIG")"
echo "litellm_url=$(jq -er '.shared_envs.default.litellm_proxy_url' "$CONFIG")"
} >> "$GITHUB_OUTPUT"
- name: Fetch sync e2e env secrets from Vault
id: sync_e2e_secrets
continue-on-error: true
if: ${{ github.event_name == 'workflow_dispatch' && inputs.run_minds_release_tests }}
uses: imbue-ai/use-vault-secrets@main
with:
role: minds_ci_test_gh
secrets: |
minds/ci/runs/${{ steps.sync_e2e_env.outputs.env_name }}/shared-default/SUPERTOKENS_CONNECTION_URI
minds/ci/runs/${{ steps.sync_e2e_env.outputs.env_name }}/shared-default/SUPERTOKENS_API_KEY
- name: Pause before the sync e2e Vault retry
if: ${{ github.event_name == 'workflow_dispatch' && inputs.run_minds_release_tests && steps.sync_e2e_secrets.outcome == 'failure' }}
run: sleep 15
- name: Fetch sync e2e env secrets from Vault (retry)
if: ${{ github.event_name == 'workflow_dispatch' && inputs.run_minds_release_tests && steps.sync_e2e_secrets.outcome == 'failure' }}
uses: imbue-ai/use-vault-secrets@main
with:
role: minds_ci_test_gh
secrets: |
minds/ci/runs/${{ steps.sync_e2e_env.outputs.env_name }}/shared-default/SUPERTOKENS_CONNECTION_URI
minds/ci/runs/${{ steps.sync_e2e_env.outputs.env_name }}/shared-default/SUPERTOKENS_API_KEY
- name: Export sync e2e env vars for the offload stage
if: ${{ github.event_name == 'workflow_dispatch' && inputs.run_minds_release_tests }}
run: |
set -euo pipefail
test -n "${SUPERTOKENS_CONNECTION_URI:-}" || { echo "Vault secrets missing after retry" >&2; exit 1; }
{
echo "MINDS_SYNC_E2E_CONNECTOR_URL=${{ steps.sync_e2e_env.outputs.connector_url }}"
echo "MINDS_SYNC_E2E_LITELLM_URL=${{ steps.sync_e2e_env.outputs.litellm_url }}"
echo "MINDS_SYNC_E2E_SUPERTOKENS_CONNECTION_URI=${SUPERTOKENS_CONNECTION_URI}"
echo "MINDS_SYNC_E2E_SUPERTOKENS_API_KEY=${SUPERTOKENS_API_KEY}"
} >> "$GITHUB_ENV"

- name: Run minds snapshot-resume tests via offload
id: tests
run: just test-offload-minds-snapshot "${{ needs.build-minds-snapshot.outputs.image_id }}"
Expand Down Expand Up @@ -835,12 +910,8 @@ jobs:
VAULT_TOKEN=$(vault write -field=token auth/jwt_github_actions/login role=minds_ci_test_gh jwt="$OIDC")
echo "::add-mask::$VAULT_TOKEN"
printf '%s' "$VAULT_TOKEN" > "$HOME/.vault-token"
- name: Download deployment env config
if: ${{ !cancelled() && github.event_name == 'workflow_dispatch' && inputs.run_minds_release_tests }}
uses: actions/download-artifact@v7
with:
name: minds-ci-env-config
path: .minds
# The env config was already downloaded to .minds/ by the sync-e2e
# plumbing step before the offload run (same opt-in gate).
- name: Run minds_services tests against the per-run ci env
id: services_tests
if: ${{ !cancelled() && github.event_name == 'workflow_dispatch' && inputs.run_minds_release_tests }}
Expand Down
13 changes: 13 additions & 0 deletions apps/minds/changelog/mngr-account-association.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Workspace/account state now syncs across a user's devices, end-to-end encrypted. Each associated workspace has a record on the Imbue Cloud connector: plaintext metadata (name, color, provider, which device hosts it) plus a secrets blob (the workspace's SSH key material and its backup `restic.env`) encrypted under a per-account data key that only the user's master password can unwrap.

The master password's only role is now wrapping that per-account key: new backup repositories are initialized with the workspace's own random password as their single key, the per-workspace "backup encryption method" / master-password inputs are gone from the create form and workspace settings, and the Settings page's password change rewraps the key (and pushes/clears synced secrets) instead of rekeying every repository. Clearing the password scrubs all synced secrets server-side; while the password is empty, only metadata syncs.

The landing page and sidebar list workspaces that live on the user's other devices as greyed rows with an "on <device>" badge and a remove-from-list action; their backup status and download still work here (the backup credentials are materialized from the synced record once the account is unlocked). A new-device unlock banner asks for the master password once and installs the account key.

Reconcile also heals a missing server-side key bundle: if this device holds the password-wrapped account key but the connector has none (the one-shot legacy conversion wraps the carried-over password locally, and only the settings password-change flow used to push bundles), the bundle is uploaded. Without this, a converted legacy install would sync its encrypted secrets while no other device -- and no reinstall after a machine loss -- could ever unlock them. An existing server bundle always wins, so a rewrap done on another device is never clobbered.

Legacy local state (`workspace_associations.json` -- or the older `sessions.json` layout when that file never existed -- plus `backup_password_hash` and `backup_password`) is converted once at startup and renamed aside with a `.pre-sync` suffix. Destroying a workspace now tombstones its record (metadata and secrets kept) so its backups remain reachable from any device.

New Electron-driven release tests (`apps/minds/test_sync_e2e.py`, run in the minds-snapshot sandbox against the per-run CI connector env on `run_minds_release_tests` runs): a full machine-loss-and-recover lifecycle (real imbue-cloud backups to R2, master password, wipe everything, sign back in, unlock, download and byte-verify the backup), the legacy-file one-shot migration, and the master-password lifecycle (rewrap-only change, clear-scrubs, re-set re-pushes).

CI cleanup now sweeps the R2 buckets imbue-cloud backups leave behind. Nothing ever deleted them (env destroy tears down Modal, Neon, SuperTokens and Cloudflare tunnels, but not buckets), so every CI run that exercised backups leaked one permanently. The `cleanup-minds-ci-envs` stage now also deletes buckets whose owning account no longer exists -- emptying each one first, since Cloudflare refuses to delete a non-empty bucket. Because the dev and CI tiers share a Cloudflare account, the rule is deliberately positive rather than heuristic: a bucket is only swept when its owner prefix matches no user in any SuperTokens app on the shared core, and the sweep fails closed (an unreachable SuperTokens, or an empty live-owner set, aborts before deleting anything) so a developer's own backups can never be caught in it.
112 changes: 112 additions & 0 deletions apps/minds/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,29 @@
import subprocess
from collections.abc import Iterator
from pathlib import Path
from uuid import uuid4

import httpx
import pytest
from loguru import logger
from pydantic import AnyUrl
from pydantic import SecretStr

from imbue.imbue_common.conftest_hooks import register_conftest_hooks
from imbue.imbue_common.conftest_hooks import register_marker
from imbue.imbue_common.primitives import NonEmptyStr
from imbue.minds.deployment_tests.helpers import create_verified_user_via_admin_api
from imbue.minds.deployment_tests.helpers import delete_user_via_admin_api
from imbue.minds.testing import SYNC_E2E_CONNECTOR_URL_ENV
from imbue.minds.testing import SYNC_E2E_LITELLM_URL_ENV
from imbue.minds.testing import SYNC_E2E_SUPERTOKENS_API_KEY_ENV
from imbue.minds.testing import SYNC_E2E_SUPERTOKENS_URI_ENV
from imbue.minds.testing import SyncE2EAccount
from imbue.minds.testing import SyncE2EEnv
from imbue.mngr.utils.logging import suppress_warnings
from imbue.mngr.utils.plugin_testing import register_plugin_test_fixtures
from imbue.mngr.utils.testing import generate_test_environment_name
from imbue.mngr.utils.testing import get_short_random_string

# Point ``MINDS_RESTIC_BINARY`` at the bundled ``resources/restic/restic``
# binary so restic_cli tests don't require a system-wide restic install.
Expand Down Expand Up @@ -92,6 +107,44 @@ def mngr_test_prefix() -> str:
return f"{generate_test_environment_name()}-"


_SNAPSHOT_START_DOCKERD_SCRIPT = Path("/code/mngr/libs/mngr/imbue/mngr/resources/start-dockerd.sh")
_SNAPSHOT_DOCKERD_STARTUP_TIMEOUT_SECONDS = 180


@pytest.fixture(scope="session")
def snapshot_sandbox_dockerd() -> None:
"""Bring ``dockerd`` back up in a snapshot-resumed sandbox.

``sandbox.snapshot_filesystem`` only captures the disk, not running
processes -- so a sandbox booted from the minds-workspace snapshot has
``/var/lib/docker`` populated (with the stopped workspace container's
image layers + on-disk state) but no ``dockerd`` running. Re-run the same
script the snapshot itself used to bring dockerd up, so ``docker``
invocations in the requesting tests can talk to the daemon.

Shared by ``test_snapshot_resume.py`` (via its module-autouse wrapper) and
``test_sync_e2e.py`` (requested explicitly). Mirrors
``_ensure_dockerd_for_release`` in ``libs/mngr/imbue/mngr/conftest.py``
but for the snapshot's in-tree script location.
"""
docker_info = subprocess.run(["docker", "info"], capture_output=True)
if docker_info.returncode == 0:
return

if not _SNAPSHOT_START_DOCKERD_SCRIPT.is_file():
raise FileNotFoundError(
f"start-dockerd.sh not found at {_SNAPSHOT_START_DOCKERD_SCRIPT}; this fixture is "
"only useful inside a sandbox booted from scripts/snapshot_minds_e2e_state.py."
)

subprocess.run(["chmod", "+x", str(_SNAPSHOT_START_DOCKERD_SCRIPT)], check=True, timeout=5)
subprocess.run(
[str(_SNAPSHOT_START_DOCKERD_SCRIPT)],
check=True,
timeout=_SNAPSHOT_DOCKERD_STARTUP_TIMEOUT_SECONDS,
)


class _XvfbStartupError(RuntimeError):
"""Raised when the Xvfb display server fails to start for an Electron test."""

Expand Down Expand Up @@ -148,3 +201,62 @@ def xvfb_display() -> Iterator[str]:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()


@pytest.fixture
def sync_e2e_env() -> SyncE2EEnv:
"""The real connector env the workspace-sync e2e tests target, or skip.

The coordinates are forwarded into the snapshot offload sandbox only on
``run_minds_release_tests`` CI runs (and can be exported by an operator
pointing at a dev env for local iteration); on every other run the vars
are absent and the sync e2e tests skip.
"""
values: dict[str, str] = {}
for env_var in (
SYNC_E2E_CONNECTOR_URL_ENV,
SYNC_E2E_LITELLM_URL_ENV,
SYNC_E2E_SUPERTOKENS_URI_ENV,
SYNC_E2E_SUPERTOKENS_API_KEY_ENV,
):
value = os.environ.get(env_var)
if not value:
pytest.skip(f"{env_var} is not set; the sync e2e tests need a real connector env")
values[env_var] = value
return SyncE2EEnv(
connector_url=values[SYNC_E2E_CONNECTOR_URL_ENV],
litellm_proxy_url=values[SYNC_E2E_LITELLM_URL_ENV],
supertokens_connection_uri=SecretStr(values[SYNC_E2E_SUPERTOKENS_URI_ENV]),
supertokens_api_key=SecretStr(values[SYNC_E2E_SUPERTOKENS_API_KEY_ENV]),
)


@pytest.fixture
def sync_e2e_account(sync_e2e_env: SyncE2EEnv) -> Iterator[SyncE2EAccount]:
"""A unique, pre-verified, paid account on the sync e2e env; deleted on teardown.

The address lives under ``imbue.com`` because the ci/dev deploy tiers seed
that domain into ``paid_domains`` -- imbue-cloud backups (R2 bucket
provisioning) are paid-gated, and these tests exercise them for real. The
account is provisioned through the SuperTokens admin API (setup machinery,
not part of the user journey under test); the tests then sign in through
the real UI with the returned email + password.
"""
email = f"sync-e2e-{get_short_random_string()}@imbue.com"
password = SecretStr(f"pw-{uuid4().hex}")
user_id, access_token = create_verified_user_via_admin_api(
connection_uri=sync_e2e_env.supertokens_connection_uri,
api_key=sync_e2e_env.supertokens_api_key,
connector_url=AnyUrl(sync_e2e_env.connector_url),
email=NonEmptyStr(email),
password=password,
)
yield SyncE2EAccount(email=email, password=password, user_id=str(user_id), access_token=access_token)
try:
delete_user_via_admin_api(
connection_uri=sync_e2e_env.supertokens_connection_uri,
api_key=sync_e2e_env.supertokens_api_key,
user_id=NonEmptyStr(str(user_id)),
)
except httpx.HTTPError as e:
logger.warning("Could not delete sync e2e account {}: {}", email, e)
22 changes: 21 additions & 1 deletion apps/minds/imbue/minds/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,15 @@
from imbue.minds.desktop_client.server import serve_desktop_client
from imbue.minds.desktop_client.session_store import MultiAccountSessionStore
from imbue.minds.desktop_client.state import get_state
from imbue.minds.desktop_client.sync_scheduler import WorkspaceSyncScheduler
from imbue.minds.desktop_client.system_interface_health import SystemInterfaceHealthTracker
from imbue.minds.desktop_client.system_interface_health import should_enroll_suspect_for_backend_failure
from imbue.minds.desktop_client.templates import DEFAULT_WORKSPACE_TEMPLATE_GIT_URL
from imbue.minds.desktop_client.templates import FALLBACK_BRANCH
from imbue.minds.desktop_client.templates import is_local_workspace_defaults_opt_in
from imbue.minds.desktop_client.workspace_record_store import WorkspaceRecordStore
from imbue.minds.desktop_client.workspace_record_store import read_device_id
from imbue.minds.desktop_client.workspace_record_store import read_device_label
from imbue.minds.envs.docker_cleanup import DockerCleanupError
from imbue.minds.envs.docker_cleanup import start_active_env_state_container
from imbue.minds.primitives import OneTimeCode
Expand Down Expand Up @@ -383,7 +387,22 @@ def run(
mngr_caller=mngr_caller,
connector_url=client_env_config.connector_url,
)
session_store = MultiAccountSessionStore(data_dir=data_directory, cli=imbue_cloud_cli)
workspace_record_store = WorkspaceRecordStore(
paths=paths,
mngr_host_dir=mngr_host_dir,
cli=imbue_cloud_cli,
device_id=read_device_id(mngr_host_dir),
device_label=read_device_label(),
)
session_store = MultiAccountSessionStore(
data_dir=data_directory, cli=imbue_cloud_cli, record_store=workspace_record_store
)
sync_scheduler = WorkspaceSyncScheduler(
record_store=workspace_record_store,
session_store=session_store,
resolver=backend_resolver,
)
sync_scheduler.start(root_concurrency_group)
response_events = load_response_events(data_directory)
request_inbox = RequestInbox()
for resp in response_events:
Expand Down Expand Up @@ -563,6 +582,7 @@ def run(
minds_api_key=minds_api_key,
latchkey_forward_supervisor=latchkey_forward_supervisor,
discovery_health_watchdog=discovery_health_watchdog,
sync_scheduler=sync_scheduler,
)

# Background loop driving the discovery-pipeline watchdog: polls snapshot
Expand Down
16 changes: 9 additions & 7 deletions apps/minds/imbue/minds/desktop_client/agent_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1404,7 +1404,7 @@ def start_creation(
branch_or_tag: str = "",
region: str = "",
anthropic_api_key: str = "",
on_created: Callable[[AgentId], None] | None = None,
on_created: Callable[[AgentId, HostId], None] | None = None,
backup_request: BackupSetupRequest | None = None,
color: str | None = None,
docker_runtime: DockerRuntime = DockerRuntime.RUNC,
Expand Down Expand Up @@ -1437,10 +1437,12 @@ def start_creation(
ask for.

When ``on_created`` is provided, it is called with the canonical
``AgentId`` once ``mngr create`` returns (immediately before the
status flips to ``DONE``). The id is parsed from the inner
``mngr create``'s JSONL ``"event": "created"`` line, not pre-generated;
for imbue_cloud agents it's the leased pool host's pre-baked id.
``AgentId`` and ``HostId`` once ``mngr create`` returns (immediately
after the status flips to ``DONE``, so consumers can rely on the
published canonical id). Both ids are parsed from the
inner ``mngr create``'s JSONL ``"event": "created"`` line, not
pre-generated; for imbue_cloud agents they are the leased pool
host's pre-baked ids.

Returns a ``CreationId`` immediately for tracking the in-flight
creation. Use ``get_creation_info()`` to poll status (and read
Expand Down Expand Up @@ -1550,7 +1552,7 @@ def _create_agent_background(
branch_or_tag: str = "",
region: str = "",
anthropic_api_key: str = "",
on_created: Callable[[AgentId], None] | None = None,
on_created: Callable[[AgentId, HostId], None] | None = None,
backup_request: BackupSetupRequest | None = None,
color: str | None = None,
docker_runtime: DockerRuntime = DockerRuntime.RUNC,
Expand Down Expand Up @@ -1889,7 +1891,7 @@ def _create_agent_background(
self._redirect_urls[cid_str] = redirect_url

if on_created is not None:
on_created(canonical_id)
on_created(canonical_id, canonical_host_id)

# Configure restic backups asynchronously on a detached
# thread (mirrors the Cloudflare tunnel-token path): bucket
Expand Down
Loading
Loading