fix(git-sync): the auto-sync toggle is authoritative and live (#3010) - #3017
Conversation
PUT /git/auto-sync wrote the DB row and nothing else: the container gated on GIT_SYNC_AUTO read once at startup, and every recreate re-derived it as DB flag OR baked env. OFF never took effect on an agent that baked the env at creation; ON waited for the next recreate. - agent loop: starts whenever it can ask the platform and reads the owner's flag every cycle via GET /git/auto-sync with its own MCP key; OFF skips the cycle, ON runs it, no recreate. 404 'Git not configured' -> off; platform unreachable / 5xx / auth refusal -> the env fallback. /api/git/status reports the value the loop runs with. - one writer: _apply_git_env_from_db derives GIT_SYNC_AUTO from the DB flag alone (OR + log removed); creation writes the flag from the same _git_auto_sync_baked predicate that bakes the env, ghosts included. - one-shot backfill (SQLite + Alembic 0075): live non-source-mode ghosts, the env-true/DB-0 slice the DB can identify. - Settings -> Git sync panel with both toggles (auto-sync, pause schedules while sync is failing). Fixes #3010 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…omments (#3010) Found on a live dev agent: the first cycle waits a full interval, so /api/git/status reported the env fallback (off) for 15 minutes while the owner's flag said on. Resolve once before the first sleep. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Live verification against the local dev instanceSetup: I built the base image from this branch as
Found live and fixed in Not covered here: the backend half of this PR (the DB-only recreate derivation, the creation write, the migration) didn't run live, because that needs the dev backend rebuilt from this branch. Unit tests and mutations cover it. Cleanup: agent, workspace volume, git-config/sync-state rows and the 🤖 Generated with Claude Code |
|
merge-train: merged Heads-up: #2984, #3000, #3005 and #3023 also declare Riding the current merge train together with #3016 (validation flagged that the live toggle should not land without #3016's shared-branch push refusal). |
…toggle # Conflicts: # docs/memory/architecture/agent-lifecycle.md # tests/registry.json
…0076_operator_queue_ask_object #3017 landed 0075_auto_sync_enabled_backfill on 0074, the same parent as 0075_operator_queue_ask_object, which made two heads: upgrade head would then apply zero revisions (#2068). Renumber ours to 0076 and parent it on dev's 0075. The two revisions touch disjoint tables (agent git config vs operator_queue), so the order carries no design decision. The SQLite migration keeps its name (operator_queue_ask_object), so a database that already applied it does not run it again; its list entry follows dev's. Point-in-time security reports keep the number they audited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…only endings, wake on any ending, self-readback (Abilityai/trinity-enterprise#611, PR A of 2) (#3023) * feat(operator-queue): ask endings — one sink, endings ledger, person-only endings, wake on any ending, self-readback (Abilityai/trinity-enterprise#611, PR A of 2) Every way an ask ends goes through one sink, services/ask_service.py: the operator answer, the Workspace answer, single cancel, bulk cancel, and the poller's expiry. Each ending runs the same four steps: - a compare-and-set that also writes the endings ledger (disposition, disposed_at, disposed_by person|timeout, disposed_by_email, disposition_reason, batch_id); - one audit row per transition; - one thin /ws trigger; - the registered ending observers, which receive only the rows this call won. What changes: - Only a person ends an ask. respond / cancel / bulk-cancel and the Workspace answer refuse agent-, system- and every other non-person key with 403 person_required (an allowlist). - An answer after the deadline is a 409 expired, even before the sweep. - The ent#329 wake now fires on any ending, under the same opt-in. A cancel or an expiry wakes the agent once per agent per event (trigger operator_ending); an expiry is framed as "Denied by timeout". - An agent reads back its own ask by request_id: new GET /api/agents/{name}/operator-queue/{request_id}, and MCP get_my_ask. - Every composed turn gets an "Ended asks" Execution Context line. - Endings show with who and when in the Operations Resolved feed, a /m "Recently ended" strip, and the Workspace (ended asks listed for 7 days with a coarse who). - Migration pair operator_queue_ask_object (SQLite) / 0075 (Alembic): 12 nullable columns. Review and CSO fixes, each with a failing test first and a red mutation: - an out-of-range deadline no longer fails ingest; - the wake's container check runs off the event loop; - an operator's Clear All no longer hides a client's ended asks; - the Clear-All confirmation promises a wake only to running agents; - a system-scoped key can no longer answer an ask through the Workspace answer route. Part of Abilityai/trinity-enterprise#611 (PR A of 2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(registry): the ent#611 suite also pins the Workspace person gate, Clear-All read-through, deadline overflow and the off-loop running check Part of Abilityai/trinity-enterprise#611 (PR A of 2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(migrations): re-parent the ent#611 revision onto dev's 0075 as 0076_operator_queue_ask_object #3017 landed 0075_auto_sync_enabled_backfill on 0074, the same parent as 0075_operator_queue_ask_object, which made two heads: upgrade head would then apply zero revisions (#2068). Renumber ours to 0076 and parent it on dev's 0075. The two revisions touch disjoint tables (agent git config vs operator_queue), so the order carries no design decision. The SQLite migration keeps its name (operator_queue_ask_object), so a database that already applied it does not run it again; its list entry follows dev's. Point-in-time security reports keep the number they audited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
PUT /api/agents/{name}/git/auto-syncwrote the DB row and nothing else. The container gated its heartbeat onGIT_SYNC_AUTO, read once at startup, and every recreate re-derived it as DB flag OR baked env. OFF never took effect on an agent that baked the env; ON waited for the next recreate.TRINITY_BACKEND_URL+ its ownTRINITY_MCP_API_KEY) and reads the owner's flag every cycle through the existingGET .../git/auto-sync. OFF skips the next cycle, ON runs it, no recreate.404 "Git not configured"→ off; platform unreachable / 5xx / auth refusal / uniform 404 → theGIT_SYNC_AUTOenv (the last value the platform handed the container)._apply_git_env_from_dbderivesGIT_SYNC_AUTOfromauto_sync_enabledalone (the OR branch and its log are gone). Creation writes the flag from the same_git_auto_sync_bakedpredicate that bakes the env, ghosts included (the oldand not config.ephemeralis what made env and DB disagree at birth).POST .../start(AuthorizedAgentByName) can't flip the owner-onlyPUT(OwnedAgentByName). With the OR gone, the recreate path has no way to re-arm a flag the owner cleared./api/git/statusreturnsauto_sync_enabled, the value the loop is running with, from the same sourceGET .../git/auto-syncreturns.BaseToggle,InlineErrorper failed save,LoadFailed/skeleton, and a "not connected" state for agents with no git binding.The AC asks for a backfill "keyed on the creation-time predicate". The DB can't reconstruct that predicate. There's no template/fork column, and agents bound later through
POST /git/initializeare alsosource_mode = 0but never baked the env. Asource_mode = 0backfill would therefore arm auto-push on agents that never had it. The migration (auto_sync_enabled_backfill+ Alembic0075) sets the flag only for the slice the DB can identify: live non-source-mode ghost agents, the population the oldand not config.ephemeralleft env-true/DB-0.Consequences:
error.Changes
docker/base-image/agent_server/auto_sync.py: per-cycleresolve_auto_sync_enabled,should_start_loop,run_one_cycle,current_auto_sync_enableddocker/base-image/agent_server/routers/git.py:auto_sync_enabledin statussrc/backend/services/agent_service/lifecycle.py: DB-only derivationsrc/backend/services/agent_service/crud.py: DB write from the bake predicatesrc/backend/db/migrations.py+migrations/versions/0075_auto_sync_enabled_backfill.py: backfill, both trackssrc/frontend/src/components/GitSyncSettingsPanel.vue,settings/SettingsPanel.vue,stores/agents.jstests/unit/test_3010_autosync_toggle.py(26, new)src/frontend/tests/unit/gitSyncSettingsPanel.spec.js(7, mounted)test_ent109_git_env_seam.pyrewritten to the new contract (recreate-after-OFF stays OFF)test_2069_gitignore_at_creation.py: DB-flag matrix (7)test_1484fixture andtest_1595loop test updated for the per-cycle gategit-sync-health.md,github-sync.md,architecture/backend.md,architecture/agent-lifecycle.mdTest Plan
test_3010,ent109×2,2069,1484,agent_server_auto_sync,1595,sync_health_service,fork_to_own,ent123,1759, migrations, schema parity,2742, dual ahead/behind,69ephemeral, alembic heads/ids)test_3010: 26 errorsent109: recreate-after-OFF fails2069: the ghost case failsvitest run3630/3630 (raw-colour, loading-gate and source-text ratchets included);check:tokensOKlint_sys_modules,lint_root_test_placementclean; single Alembic headPUTthe toggle, next cycle skips/runs (see comment)Fixes #3010
🤖 Generated with Claude Code