feat(connectors): emit connector.activation.changed for CLI/SDK activation writes (#1226)#1309
Conversation
|
Solid, focused PR that closes a real UX gap cleanly. The centralization strategy is correct, the dedup mechanism is provably race-free, and the tests cover all the edge cases that matter. Three cosmetic nits below, none blocking. SummaryCLI/SDK activation writes now flow through Issues Found🟢 Both files define identical classes and fixtures. These belong in Then remove the duplicate class + fixture from both test files and import nothing — pytest discovers the conftest fixture automatically. 🟢 Issue references in inline comments ( CLAUDE.md: "Don't reference the current task, fix, or callers inline … they belong in the PR description and commit body." The 🟢 Pre-existing omission (not introduced here), but since this PR is already touching both functions it's a good moment to make them symmetric. No suggestion required — just noting it for awareness. Strengths
VerdictApprove. The nits are cosmetic and don't affect correctness or behavior. The core implementation is well-reasoned, the dedup analysis is correct, and the test coverage is thorough. The fixture consolidation is a nice-to-have that can be done as a follow-up. |
Summary
CLI and SDK activation toggles now drive the same live
connector.activation.changedSSE update that the HTTP router already emitted, so the Agent UI's "Active for" panel reflects them without a manual refresh.Why
Activation writes from
gaia connectors activations activate/deactivate(CLI) and direct SDK calls landed in~/.gaia/connectors/activations.jsonsilently — only the HTTPPUT/DELETEhandlers emittedconnector.activation.changed. With the Agent UI Settings tab open, a CLI-driven toggle took effect on disk but the panel kept showing stale state until the user navigated away and back to force a refetch, making CLI ↔ UI workflows confusing. This was filed as an explicit follow-up on #1219.Linked issue
Closes #1226
Changes
src/gaia/connectors/api.py).activate()/deactivate()now emitconnector.activation.changed({connector_id, agent_id, active}) after the ledger write, so HTTP, SDK, and any in-process caller all notify through one path — mirroring how feat(connectors): per-agent MCP tool-visibility activations (#1005) #1219 centralized the MCP-only guard inapi.py.emit_change()helper (src/gaia/connectors/events.py). A sync, loop-aware fire-and-forget wrapper so synchronous callers (api, CLI) can publish without being async. Inside the server loop it schedules on that loop and fans out to SSE subscribers; in a bare process it runs against the registered emitter (the no-op logging emitter). Failures are logged, never swallowed.src/gaia/connectors/activation_watcher.py, new). The CLI runs in a separate process from the UI server, where the SSE bus lives. A background watcher pollsactivations.json, diffs against a snapshot, and emits one event per changed(connector, agent)pair (~1 s) so cross-process CLI/SDK writes reach connected UI clients. In-process writes callnote_local_write(...)to advance the snapshot per-pair, so they don't double-emit — and a concurrent change to a different pair is still caught.src/gaia/ui/server.py). Started/stopped in the FastAPI lifespan, mirroring the existingDocumentMonitor.src/gaia/ui/routers/connectors.py). Removed the now-duplicate inline emits fromPUT/DELETE;DELETEnow routes throughapi.deactivateinstead of the bare ledger call, closing a pre-existing MCP-only-guard bypass on that route.tests/unit/connectors/test_activation_watcher.py(diff, poll-emit, dedup, don't-mask-other-pair) and extendedtests/unit/connectors/test_activation_api.py(CLI→SSE path: emits on success, nothing on failure/rejection).docs/sdk/infrastructure/connectors.mdx). Updated the activation SSE section to describe the centralized emit + watcher. No frontend changes —useConnectorsSSE.tsalready consumesconnector.activation.changed.Test plan
python util/lint.py --all— all quality checks pass (Black, isort, Pylint, Flake8 green; pre-existing MyPy/Bandit/agent-convention warnings only, none in touched files).python -m pytest tests/unit/connectors/test_activation_api.py tests/unit/connectors/test_activation_watcher.py tests/unit/connectors/test_router_connectors.py— 77 passed.python -m pytest tests/unit/connectors/— 436 passed, 3 skipped.gaia connectors activations activate mcp-github builtin:connectors-demo --scopes use→ panel flips to active with no refresh;gaia connectors activations deactivate mcp-github builtin:connectors-demo→ flips back live.Follow-ups (not in this PR — gaps found while doing this work)
DispatchQueueworker logs after the test stream closes (pre-existing). Any UI test that boots aTestClientprints repeated--- Logging error --- ValueError: I/O operation on closed fileblocks pointing atsrc/gaia/ui/server.py:267(_import_moduleslogging the faiss-load line). Root cause:DispatchQueue.shutdown(src/gaia/ui/dispatch.py:104-105) callsself._executor.shutdown(wait=False), so a boot-init worker thread (the slowfaissimport) is still running and logs after pytest closed the captured stream. Cosmetic (tests pass, exit 0) and reproduces onmainwith this PR stashed. Suggested fix: haveshutdowndrain in-flight boot jobs with a bounded wait, e.g.await loop.run_in_executor(None, lambda: self._executor.shutdown(wait=True, cancel_futures=True))under a timeout, so no worker logs post-teardown.StarletteDeprecationWarning: Using httpx with starlette.testclient is deprecated; install httpx2(pre-existing, repo-wide). Emitted by any test using the sharedui_api_clientTestClientfixture (origintests/conftest.py:355); pytest dedupes it to a single "1 warning" line. Not introduced here (reproduces onmainwith this PR stashed). Suggested fix: bump tohttpx2per Starlette's guidance, or add a scopedfilterwarningsentry inpyproject.toml.Checklist
Closes #1226).python util/lint.py --all,pytest tests/unit/).