Skip to content

feat(circleback): add Circleback meeting notes & transcript connector - #75

Open
jqueguiner wants to merge 1 commit into
MaximeGaudin:mainfrom
jqueguiner:feat/circleback-connector
Open

feat(circleback): add Circleback meeting notes & transcript connector#75
jqueguiner wants to merge 1 commit into
MaximeGaudin:mainfrom
jqueguiner:feat/circleback-connector

Conversation

@jqueguiner

Copy link
Copy Markdown

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:

  • one message per transcript turn, attributed to its speaker and timestamped from the meeting start plus the turn offset, so ordering matches the recording (skip them with include_transcript = false);
  • an action items message rendered as a checklist with each assignee;
  • a notes message (title, duration, attendees, meeting URL, then Circleback's summary) placed last, so the inbox row previews the summary instead of an arbitrary sentence of small talk.
[[connections]]
id = "circleback"
type = "circleback"
api_key = "cb_..."
backfill_days = 365      # default
include_transcript = true # default

void setup has a wizard that validates the key against the API before writing anything. void send and void reply refuse a Circleback conversation: the source is read-only.

How it syncs

  • Meetings still being processed by Circleback 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 per-meeting sync state); 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 at 350 ms and retry on HTTP 429 honoring 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.
  • Default poll interval 900 s, overridable with 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 (wiremock for the HTTP surface).
  • crates/void-cli: plugin settings parsing, defaults, and redaction in show_config.
  • ./scripts/check.sh passes (fmt, clippy -D warnings, full test suite).
  • Ran against a real Circleback account, 21-day backfill: 104 meetings seen, 39 imported, 6 860 transcript turns, in 46 s. void inbox --connector circleback, void messages <id> and void search all return the expected content, and a second run imports nothing new.

Notes for review

  • The API paths are GET /meetings?cursor=… (20 per page, Link: rel="next"), GET /meeting/{id} and GET /meeting/{id}/transcript. The meeting path is singular — the plural form 404s.
  • duration and attendees are carried in the conversation metadata; context_id is the Circleback meeting id, so archiving a meeting dismisses its whole group.

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.
@jqueguiner

Copy link
Copy Markdown
Author

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 MaximeGaudin left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.toml lists 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 reqwest GET calls to circleback.ai/api/* with bearer auth. No URL literals beyond the documented API base.
  • Credentials: API key redacted in show_config via redact_token. Never logged or printed in clear.
  • Exec / unsafe / filesystem: None.
  • cargo audit: only pre-existing warnings (bincode, event-listener, chacha20 — all from wa-rs chain, 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:

  1. Hand-rolled urlencode in api.rs:276 — the workspace ships urlencoding = "2" and 5 other crates use it. Should use the workspace crate for consistency and correctness.
  2. pub(crate) mod circleback in connectors/mod.rs — only connector with this visibility, needed because setup/circleback.rs imports DEFAULT_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 urlencoding crate instead of hand-rolled urlencodecrates/void-circleback/src/api.rs:276. Five other crates already use urlencoding::encode.
  • Add a 429 retry wiremock testcrates/void-circleback/src/api.rs:179-190. Confirm the client retries and succeeds on the next attempt.

Nits

  • pub(crate) visibilitycrates/void-cli/src/connectors/mod.rs. Consider moving DEFAULT_BACKFILL_DAYS to the void-circleback crate (next to CONNECTOR_ID) so the descriptor stays mod circleback like every other.
  • Transcript backfill gap — Enabling include_transcript after initial import won't retroactively fetch transcripts for already-imported meetings (sync.rs:199 short-circuits on version match). Worth a one-line note in docs/connectors.md: "To fetch transcripts for meetings imported without them, clear the connector's sync state with void 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants