feat(circleback): add Circleback meeting notes & transcript connector - #75
feat(circleback): add Circleback meeting notes & transcript connector#75jqueguiner wants to merge 1 commit into
Conversation
Meetings are where most decisions are made, but they were the one thing void could not see. This adds a read-only Circleback connector so past meetings sit in the same inbox and the same search index as messages. Each meeting becomes one conversation carrying, in timeline order: - every transcript turn, attributed to its speaker (skippable with `include_transcript = false`), timestamped from the meeting start plus the turn offset so the ordering matches the recording; - an action-items message, rendered as a checklist with the assignee of each item; - a notes message (title, duration, attendees, meeting URL, then Circleback's summary) placed last so the inbox row shows the summary rather than an arbitrary sentence of small talk. Sync details: - Meetings Circleback is still processing are skipped and picked up on a later poll, so a half-written summary never lands in the store. - A meeting already imported is re-imported only when its `updatedAt` changes, tracked as sync state per meeting; a transcript is fetched at most once. - The incremental pass re-reads a 14-day window behind the last poll, which is what it takes to catch a meeting whose notes are finalized days after the call. - Requests are paced (350 ms) and retry on HTTP 429 honoring `Retry-After`, within Circleback's free-tier limits; an invalid key fails the health check with a clear message instead of an empty sync. Settings: `api_key` (required), `backfill_days` (default 365), `include_transcript` (default true). `void setup` gains a wizard that validates the key against the API before writing the config. `void send` and `void reply` refuse a Circleback conversation: the connector is read-only by design.
|
Refs #76 — opened an issue describing the service, auth model and polling approach, as CONTRIBUTING.md asks for new connectors. Happy to reshape the data model there before this gets reviewed in depth; in particular whether one message per transcript turn is the right granularity, or whether the whole transcript should be a single message. |
MaximeGaudin
left a comment
There was a problem hiding this comment.
PR Review — #75: feat(circleback): add Circleback meeting notes & transcript connector
Recommendation: Merge with minor fixes
Author: jqueguiner · +1478/−6 across 18 files · CI: all pass
1. Intent & fit
Fits. New read-only connector following docs/adding-a-connector.md exactly. Issue #76 opened beforehand as CONTRIBUTING requests. Single focused commit, conventional commit style. CHANGELOG, README, docs/commands.md, docs/configuration.md, and docs/connectors.md all updated. Cross-platform: only workspace deps, no OS-specific code.
2. Security · Verdict: clean
- Dependencies: No new external crates.
void-circleback/Cargo.tomllists only workspace deps (void-core,tokio,serde,serde_json,tracing,anyhow,async-trait,tokio-util,reqwest,chrono; dev:wiremock,tempfile). Lockfile adds only the workspace crate itself. - CI/Workflows: No
.github/changes. - Network surface: Only
reqwestGET calls tocircleback.ai/api/*with bearer auth. No URL literals beyond the documented API base. - Credentials: API key redacted in
show_configviaredact_token. Never logged or printed in clear. - Exec / unsafe / filesystem: None.
cargo audit: only pre-existing warnings (bincode, event-listener, chacha20 — all fromwa-rschain, unrelated).cargo deny check advisories licenses: pass.
No security concerns found.
3. Code review
Implementation patterns · minor drift
Follows the adding-a-connector checklist precisely. Structure mirrors void-hackernews. Sync loop pattern matches HN/Gmail/Slack conventions.
Two deviations:
- Hand-rolled
urlencodeinapi.rs:276— the workspace shipsurlencoding = "2"and 5 other crates use it. Should use the workspace crate for consistency and correctness. pub(crate) mod circlebackinconnectors/mod.rs— only connector with this visibility, needed becausesetup/circleback.rsimportsDEFAULT_BACKFILL_DAYS. Every other connector keeps defaults private. Minor; arguably cleaner than duplicating the constant.
Test coverage · adequate
15 tests across 3 files covering API paging, cursor extraction, 404/auth rejection, message builders (notes, action items, transcript), settings parsing, defaults, and redaction.
Missing but not blocking:
- No 429 retry/backoff test (
api.rs:179-190). The retry logic is untested. Should-fix.
Blockers
None.
Should-fix
- Use workspace
urlencodingcrate instead of hand-rolledurlencode—crates/void-circleback/src/api.rs:276. Five other crates already useurlencoding::encode. - Add a 429 retry wiremock test —
crates/void-circleback/src/api.rs:179-190. Confirm the client retries and succeeds on the next attempt.
Nits
pub(crate)visibility —crates/void-cli/src/connectors/mod.rs. Consider movingDEFAULT_BACKFILL_DAYSto thevoid-circlebackcrate (next toCONNECTOR_ID) so the descriptor staysmod circlebacklike every other.- Transcript backfill gap — Enabling
include_transcriptafter initial import won't retroactively fetch transcripts for already-imported meetings (sync.rs:199short-circuits on version match). Worth a one-line note indocs/connectors.md: "To fetch transcripts for meetings imported without them, clear the connector's sync state withvoid sync --clear-connector circleback."
Summary
Clean, well-structured connector that follows every documented convention. No security concerns — no new deps, no exec, no credential exposure. Two should-fix items (use workspace urlencoding crate; add a 429 retry test) and two nits. Neither blocks merge. Quality matches or exceeds existing connectors in the repo.
What
A read-only connector for Circleback, the meeting notetaker. Every meeting becomes a conversation in the same inbox and the same FTS index as messages, so what was said in a call is searchable next to what was written in Slack or Gmail.
Each meeting yields, in timeline order:
include_transcript = false);void setuphas a wizard that validates the key against the API before writing anything.void sendandvoid replyrefuse a Circleback conversation: the source is read-only.How it syncs
updatedAtchanges (tracked as per-meeting sync state); a transcript is fetched at most once.Retry-After, inside Circleback's free-tier limits (3 req/s, 20/min). An invalid key fails the health check with a clear message instead of an empty sync.circleback_poll_interval_secs.Testing
crates/void-circleback: 11 unit tests — API paging and cursor extraction, 404 and 429 handling, rejected key, and the three message builders (wiremockfor the HTTP surface).crates/void-cli: plugin settings parsing, defaults, and redaction inshow_config../scripts/check.shpasses (fmt,clippy -D warnings, full test suite).void inbox --connector circleback,void messages <id>andvoid searchall return the expected content, and a second run imports nothing new.Notes for review
GET /meetings?cursor=…(20 per page,Link: rel="next"),GET /meeting/{id}andGET /meeting/{id}/transcript. The meeting path is singular — the plural form 404s.durationandattendeesare carried in the conversation metadata;context_idis the Circleback meeting id, so archiving a meeting dismisses its whole group.