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. |
itomek
left a comment
There was a problem hiding this comment.
Approve. I reproduced the test and lint evidence locally on the PR branch and traced the change against issue #1226's acceptance criteria; this is solid, well-scoped plumbing that does exactly what the description says.
Independent verification:
- Reran the new suites on this branch: the 4 API emit tests (activate emits
active:true, deactivate emitsactive:false, failed activate emits nothing, rejected-OAuth activate emits nothing) and the 5 watcher tests (external activate/deactivate, no-change emits nothing,note_local_writesuppresses the duplicate, andnote_local_writedoes not mask an unrelated pair) all pass — 27 passed across the two new/changed files, 316 passed / 3 skipped for the broader connectors suite. black + isort clean. - ACs mapped: AC1/AC2/AC4 are directly covered by the new unit tests; AC3 (live UI refresh without nav) is cross-process and only manually verifiable, which your PR body documents —
TestPollOncecovers it indirectly. Good enough. - One note for anyone seeing local failures:
test_router_connectors.pyerrors here from a missingpython-multipart, but that reproduces onmaintoo and CI's unit-test check is green — pre-existing env gap, not a PR defect.
One thing worth calling out as a strength: routing the DELETE path through deactivate_connector_for_agent(...) instead of the bare deactivate_agent(...) ledger call (routers/connectors.py:829) means the MCP-only activation guard that api.deactivate() enforces now also applies on the HTTP delete path — previously DELETE bypassed it. The description frames this as cleanup; can you confirm closing that guard gap on DELETE was intentional and not just an incidental side effect of the refactor? Worth a line in the body so it isn't lost.
One non-blocking question: in emit_change() the no-loop branch swallows the failure (except Exception: logger.exception(...)) rather than re-raising. I think that's the right call here — the SSE notification is non-transactional with respect to the committed ledger write, and you don't want a notification hiccup to fail an otherwise-successful write — so I'd keep it. Just flagging it against the "fail loudly" convention so it's a conscious choice; a one-line why-comment naming that invariant would settle it.
On the cosmetic points already raised in the automated review (the duplicated _RecordingEmitter/fixture, the inline (#1226) references, and the deactivate() log asymmetry) — I concur, no need to repeat them; worth folding into the same touch-up pass.
Nice that this cleanly unblocks the stacked #1310. Thanks @alexey-tyurin.
Generated by Claude Code
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/).