Skip to content

feat: add generic single-meeting ingestion endpoint - #500

Merged
wesm merged 2 commits into
kenn-io:mainfrom
salmonumbrella:feat/generic-meeting-ingestion
Jul 30, 2026
Merged

feat: add generic single-meeting ingestion endpoint#500
wesm merged 2 commits into
kenn-io:mainfrom
salmonumbrella:feat/generic-meeting-ingestion

Conversation

@salmonumbrella

Copy link
Copy Markdown
Contributor

What changed

  • Adds authenticated POST /api/v1/import/meeting for one provider-neutral meeting.
  • Stores imports as canonical searchable meeting transcripts with stable update-in-place IDs.
  • Includes imported sources in Meetings mode and publishes the OpenAPI schema, generated Go client, and usage docs.

Why

Local transcription tools and automations currently have no supported way to archive one completed meeting without a provider-specific integration.

Usage

POST one JSON meeting with a stable source.identifier and meeting.external_id. The first delivery returns 201 created; later deliveries replace the same archived meeting and return 200 updated.

@roborev-ci

roborev-ci Bot commented Jul 23, 2026

Copy link
Copy Markdown

roborev: Combined Review (0bdee33)

The change is generally sound, but two medium-severity reliability and data-integrity issues should be fixed.

Medium

  • internal/meetingimport/decode.go:34 — Metadata numbers decode as float64, silently corrupting integers above 2^53 when provider metadata and the canonical raw record are re-marshaled. Call decoder.UseNumber() and add coverage for large nested numeric identifiers.

  • cmd/msgvault/cmd/serve.go:360 — Cache refresh runs synchronously under the endpoint’s default 60-second request context. Existing-meeting updates may require a full analytics rebuild, causing large archives to repeatedly terminate the rebuild and return an error after the durable write. Treat /api/v1/import/meeting as a long-running daemon request, or give it a dedicated timeout sufficient for cache rebuilding while retaining disconnect cancellation.


Reviewers: 2 done | Synthesis: codex, 12s | Total: 7m59s

@salmonumbrella

Copy link
Copy Markdown
Contributor Author

Fixed in commit 253c620:

  • Added decoder.UseNumber() and regression coverage for nested metadata identifiers above 2^53, preserving exact values through canonical JSON marshaling.
  • Classified /api/v1/import/meeting as a long-running daemon request. It now avoids the default 60-second context and write deadline while retaining request-context cancellation on client disconnect.

Validation: make test, make lint-ci, and go vet -tags "fts5 sqlite_vec" ./... pass.

@roborev-ci

roborev-ci Bot commented Jul 23, 2026

Copy link
Copy Markdown

roborev: Combined Review (253c620)

No issues found.


Reviewers: 2 done | Synthesis: codex | Total: 8m42s

@roborev-ci

roborev-ci Bot commented Jul 24, 2026

Copy link
Copy Markdown

roborev: Combined Review (253c620)

No issues found.


Reviewers: 2 done | Synthesis: codex | Total: 5m10s

@wesm

wesm commented Jul 25, 2026

Copy link
Copy Markdown
Member

will review

@roborev-ci

roborev-ci Bot commented Jul 26, 2026

Copy link
Copy Markdown

roborev: Combined Review (ecda580)

Code is generally sound, but the generated browser API contract is stale.

Medium

  • web/src/lib/api/generated/schema.d.ts:6 — The generated TypeScript definitions omit the new meeting-import path and schemas defined in api/openapi.yaml. Run make web-generate and commit the updated output.

Reviewers: 2 done | Synthesis: codex, 5s | Total: 7m41s

@wesm

wesm commented Jul 27, 2026

Copy link
Copy Markdown
Member

will review!

@wesm
wesm force-pushed the feat/generic-meeting-ingestion branch from ecda580 to 49df40c Compare July 28, 2026 01:04
@roborev-ci

roborev-ci Bot commented Jul 28, 2026

Copy link
Copy Markdown

roborev: Combined Review (49df40c)

Medium-severity issue found: meeting_import sources can incorrectly sync unrelated accounts.

Medium

  • Non-schedulable source misclassifiedinternal/meetingimport/models.go:16, internal/api/handlers.go:1375

    If a meeting_import identifier matches a scheduled Gmail/IMAP account, its status incorrectly enables Sync now, which then syncs the unrelated account.

    Fix: Add explicit account/generic/non-schedulable source classification, classify meeting_import as non-schedulable, and add an identifier-collision regression test.


Reviewers: 2 done | Synthesis: codex, 15s | Total: 12m4s

@roborev-ci

roborev-ci Bot commented Jul 28, 2026

Copy link
Copy Markdown

roborev: Combined Review (01d96ba)

The PR has two medium-severity regressions despite otherwise sound validation, persistence, and scheduling safeguards.

Medium

  • internal/api/scheduler_jobs.go:41 — The account-scheduler allowlist omits teams and discord, although both use standard [[accounts]] scheduling. Their status is incorrectly reported as source_not_schedulable, and the Web UI’s “Sync Now” request returns HTTP 400.

    • Fix: Classify both as sourceScheduleAccount and add status/trigger regression tests.
  • internal/meetingimport/importer.go:202 — Re-importing an unchanged meeting always records MessagesUpdated = 1. This advances the update watermark and unnecessarily triggers full analytics-cache rebuilds during idempotent retries.

    • Fix: Compare the canonical snapshot with the stored meeting, persisting and counting an update only when data changed while preserving existing response semantics for unchanged retries.

Reviewers: 2 done | Synthesis: codex, 10s | Total: 7m27s

@roborev-ci

roborev-ci Bot commented Jul 28, 2026

Copy link
Copy Markdown

roborev: Combined Review (d93b62a)

One medium-severity reliability issue remains in the meeting-import retry path.

Medium

  • internal/meetingimport/importer.go:111 — The unchanged-raw fast path skips RecomputeConversationStats. If an earlier import persists the message but fails while updating its checkpoint or recomputing stats, an identical retry can report success while leaving message_count, participant_count, and preview fields stale.
    • Suggested fix: Recompute conversation stats before completing the unchanged-raw sync path, and add a retry test covering failure after message persistence.

Reviewers: 2 done | Synthesis: codex, 11s | Total: 7m28s

@roborev-ci

roborev-ci Bot commented Jul 29, 2026

Copy link
Copy Markdown

roborev: Combined Review (d966f97)

The implementation is functionally sound, but meeting imports introduce a medium-severity performance issue.

Medium

  • Quadratic statistics recomputationinternal/meetingimport/importer.go:119, internal/meetingimport/importer.go:243
    Each single-meeting import recomputes statistics for every conversation in the source. Repeated imports can therefore cause quadratic database work, while unchanged retries unnecessarily rewrite all accumulated conversation statistics. Recompute only the affected conversation using the existing or persisted message’s conversation ID; reserve source-wide recomputation for explicit repair operations.

Reviewers: 2 done | Synthesis: codex, 6s | Total: 12m30s

@roborev-ci

roborev-ci Bot commented Jul 29, 2026

Copy link
Copy Markdown

roborev: Combined Review (10dff1f)

Code is generally sound, but one medium-severity retry/idempotency issue can leave sender attribution stale.

Medium

  • internal/meetingimport/importer.go:110 — The unchanged-retry shortcut compares only meeting JSON. If source.account_email changes to match the organizer, the new identity is recorded but persistence is skipped, leaving messages.is_from_me at its previous value. Include derived attribution such as IsFromMe in the snapshot comparison, or verify it before taking the no-op path. Add an account-email-only retry test.

Reviewers: 2 done | Synthesis: codex, 8s | Total: 7m31s

@roborev-ci

roborev-ci Bot commented Jul 29, 2026

Copy link
Copy Markdown

roborev: Combined Review (009f7e4)

The meeting-import path is generally sound, but two medium-severity correctness and timeout issues remain.

Medium

  • Incomplete attribution refreshinternal/meetingimport/importer.go:78
    Confirming a new account_email changes identity for the entire source, but is_from_me is recalculated only for the currently imported meeting. Earlier meetings from that address remain misattributed in the primary store, affecting PostgreSQL/live-store queries and other consumers.
    Fix: When adding an identity, recompute attribution for all matching messages in that source and add a multi-meeting identity-change test.

  • Read timeout may reject valid importsinternal/api/server.go:704
    The import route clears only the write deadline, leaving the server’s 15-second ReadTimeout active. Requests up to 16 MiB may also wait up to 10 seconds for the operation gate, so slower valid uploads can time out during decoding and be reported as malformed JSON.
    Fix: Apply a sufficiently long dedicated read deadline, or read and validate the bounded request body before acquiring the operation gate.


Reviewers: 2 done | Synthesis: codex, 9s | Total: 11m0s

@roborev-ci

roborev-ci Bot commented Jul 29, 2026

Copy link
Copy Markdown

roborev: Combined Review (e3919af)

No issues found.


Reviewers: 2 done | Synthesis: codex | Total: 14m10s

@roborev-ci

roborev-ci Bot commented Jul 29, 2026

Copy link
Copy Markdown

roborev: Combined Review (3075c65)

Code review found two medium-severity issues involving transaction cleanup and stale message attribution.

Medium

  • internal/store/sync.go:193StartSyncContext uses a cancelable request context for a manually managed transaction and its deferred rollback. If cancellation occurs after BEGIN, ROLLBACK can fail, potentially returning a connection with an open transaction and retained database locks. Use a fresh bounded context for rollback, or a transaction abstraction that guarantees rollback independently of request cancellation.

  • internal/store/messages.go:817 — Adding an account identity permanently sets matching messages’ is_from_me flags, but removing the identity does not clear or recompute them. Because cache export trusts an already-true flag, an add/remove cycle can leave messages incorrectly attributed indefinitely. Preserve source-native attribution separately and recompute the effective value from remaining identities, or derive identity-based attribution dynamically.


Reviewers: 2 done | Synthesis: codex, 11s | Total: 11m23s

@roborev-ci

roborev-ci Bot commented Jul 29, 2026

Copy link
Copy Markdown

roborev: Combined Review (ba1c802)

Code is generally sound, but one medium-severity migration issue could preserve incorrect sender attribution.

Medium

  • internal/store/messages.go:846 — Legacy Granola and Circleback messages have source_is_from_me = NULL, even though their existing is_from_me values were identity-derived. The fallback interprets this as source-native, so removing a previously confirmed identity can leave messages permanently marked is_from_me; cache rebuilds also retain the incorrect value.
    • Fix: During migration, backfill provenance for sources with identity-derived attribution by setting source_is_from_me = FALSE and identity_is_from_me = is_from_me before identities can be removed.

Reviewers: 2 done | Synthesis: codex, 15s | Total: 12m12s

@roborev-ci

roborev-ci Bot commented Jul 29, 2026

Copy link
Copy Markdown

roborev: Combined Review (affa57b)

Code is generally sound, but one medium-severity cancellation/atomicity issue remains.

Medium

  • internal/meetingimport/importer.go:161 — Organizer and attendee resolution uses non-contextual EnsureParticipant calls without checking ctx inside the attendee loop. A canceled request with a large valid attendee list can continue inserting participants while holding the serial operation gate, with cancellation detected only before final persistence and partial participant inserts left behind.
    • Fix: Use context-aware participant resolution, check cancellation during iteration, and preferably include participant creation and message persistence in a batch or transaction.

Reviewers: 2 done | Synthesis: codex, 9s | Total: 10m51s

@roborev-ci

roborev-ci Bot commented Jul 30, 2026

Copy link
Copy Markdown

roborev: Combined Review (0543fc5)

Code looks clean overall; no findings at Medium, High, or Critical severity.


Reviewers: 2 done | Synthesis: codex, 9s | Total: 19m8s

@roborev-ci

roborev-ci Bot commented Jul 30, 2026

Copy link
Copy Markdown

roborev: Combined Review (3036e8f)

Code is generally sound, but one medium-severity calendar attribution regression needs attention.

Medium

  • internal/calsync/persist.go:111 — Events matching AccountEmail are marked identity-derived, but calendar source creation does not record that address in account_identities. When a manual calendar identity is added or removed, attribution refresh derives ownership solely from that table and may incorrectly clear is_from_me for events organized by the configured account.

    Suggested fix: Confirm AccountEmail when creating or updating calendar sources, migrate existing sources, or treat the configured-account match as source-derived. Add a regression test covering identity mutation after calendar ingestion.


Reviewers: 2 done | Synthesis: codex, 10s | Total: 13m13s

@roborev-ci

roborev-ci Bot commented Jul 30, 2026

Copy link
Copy Markdown

roborev: Combined Review (b7d669a)

Overall verdict: Two medium-severity issues should be addressed; no security vulnerabilities were identified.

Medium

  • Schema-dependent corruption in CopySubsetinternal/store/schema.sql:133
    Upgraded databases append the new attribution columns to messages, while fresh databases place them beside is_from_me. Because CopySubset uses INSERT INTO messages SELECT * FROM src.messages, columns can be mapped to incorrect positions, causing failures or silent data corruption. Copy with explicit source and destination column lists, and add a regression test covering a pre-change schema upgraded through InitSchema.

  • Blanket attribution updates may cause excessive writes and timeoutsinternal/store/messages.go:855
    Adding or removing an identity updates every message, even when attribution is unchanged. This fires the last_modified trigger for every row, potentially creating a large write/WAL operation and exceeding PostgreSQL statement timeouts on large archives. Initialize legacy provenance once through the maintenance migration, then limit runtime updates to rows whose source or identity-derived values actually change.


Reviewers: 2 done | Synthesis: codex, 9s | Total: 11m16s

@roborev-ci

roborev-ci Bot commented Jul 30, 2026

Copy link
Copy Markdown

roborev: Combined Review (2608f43)

Code changes are generally sound, but message attribution can be lost during ingestion or re-sync.

Medium

  • internal/store/messages.go:635 — Identity-derived attribution is overwritten on upsert. Every message upsert sets identity_is_from_me from the incoming Message, while most ingestion paths do not consult account_identities or populate IdentityDerivedIsFromMe. Messages imported after identity confirmation therefore remain unattributed, and re-syncing repaired messages can clear their attribution. Re-adding an existing identity does not trigger repair because refresh runs only for newly inserted identities.

    Recommended fix: During persistence, recompute identity-derived attribution using the final sender and current source identities, then combine it with source-native attribution. Add tests covering initial import and re-upsert after identity confirmation.


Reviewers: 2 done | Synthesis: codex, 14s | Total: 12m6s

@wesm
wesm force-pushed the feat/generic-meeting-ingestion branch from 2608f43 to b287c24 Compare July 30, 2026 14:50
@roborev-ci

roborev-ci Bot commented Jul 30, 2026

Copy link
Copy Markdown

roborev: Combined Review (b287c24)

Code is generally sound, but the legacy Google Calendar attribution migration needs correction.

Medium

  • internal/store/migrations.go:39 — Legacy Google Calendar attribution is misclassified. The provenance migration treats every legacy gcal is_from_me value as source-native, although Calendar previously also set it when the organizer email matched the configured account—an identity-derived attribution under the new logic. After an upgrade, removing that identity cannot clear attribution on unchanged historical events.

    Fix: Backfill gcal rows using organizer/account identity while preserving Organizer.Self as source-native, and add an upgrade test covering identity removal.


Reviewers: 2 done | Synthesis: codex, 10s | Total: 10m44s

@roborev-ci

roborev-ci Bot commented Jul 30, 2026

Copy link
Copy Markdown

roborev: Combined Review (56ce605)

Code is generally sound, but one medium-severity attribution consistency issue remains.

Medium

  • internal/store/messages.go:668 — Identity-derived attribution is persisted in messages.is_from_me, but participant merges and identifier reassignment can change whether a sender matches an account identity without recomputing it. A previously matched message may therefore remain permanently attributed to the user, and cache rebuilding cannot fix it because it ORs current identity matches with the stale effective value.
    • Fix: Refresh attribution for affected messages within participant-merge and identifier-mutation transactions. Derive cached attribution from source_is_from_me plus current identity matches instead of the persisted effective value.

Reviewers: 2 done | Synthesis: codex, 9s | Total: 13m12s

@roborev-ci

roborev-ci Bot commented Jul 30, 2026

Copy link
Copy Markdown

roborev: Combined Review (0805d47)

Code review found two medium-severity cache and identity-provenance migration issues; no security vulnerabilities were identified.

Medium

  • cmd/msgvault/cmd/build_cache.go:1081 — Parquet is_from_me now derives from source_is_from_me, but the provenance migration advances neither the cache schema version nor an identity revision. Existing caches may remain “ready” with stale pre-migration attribution. Bump query.CacheSchemaVersion, or atomically advance the account-identity revision when attribution changes to force a full rebuild.

  • internal/store/migrations.go:58 — The v2 provenance migration reconciles identities only for Granola, Circleback, and selected Google Calendar sources. Confirmed identities for Gmail, IMAP, MBOX, and other sources can remain identity_is_from_me=false; the legacy-config migration also inserts identities without refreshing messages. Reconcile every source with account identities during migration and refresh affected sources whenever MigrateLegacyIdentityConfig inserts identities.


Reviewers: 2 done | Synthesis: codex, 10s | Total: 10m55s

Generic single-meeting ingestion needs one coherent change so the daemon API, persistence layer, TUI presentation, generated clients, and published contract evolve together. Keeping the browser contract generated from the same OpenAPI source prevents typed web consumers from silently missing the endpoint.

Follow-up fixes consolidated:

- keep imported sources out of account scheduling
- preserve scheduled Teams and Discord sources
- keep unchanged retries idempotent while repairing statistics
- scope statistics recomputation to the affected conversation
- refresh and preserve identity-derived attribution
- support slow bounded uploads without weakening unauthenticated limits
- make the import lifecycle and participant persistence cancellation-safe
- preserve reversible source and identity attribution provenance
- migrate legacy meeting attribution safely across SQLite and PostgreSQL
- preserve configured calendar-account attribution
- copy subset messages safely across upgraded schema column order
- avoid no-op source-wide attribution rewrites
- derive attribution atomically during initial ingestion and re-sync
- preserve legacy Calendar organizer attribution provenance
- refresh attribution after participant and identifier repoints
- reconcile confirmed identities during upgrades and invalidate stale caches

Generated with Codex

Co-authored-by: Wes McKinney <wesmckinn+git@gmail.com>
Co-authored-by: Codex <noreply@openai.com>
@wesm
wesm force-pushed the feat/generic-meeting-ingestion branch from 0805d47 to f531dbb Compare July 30, 2026 19:09
@roborev-ci

roborev-ci Bot commented Jul 30, 2026

Copy link
Copy Markdown

roborev: Combined Review (f531dbb)

No issues found.


Reviewers: 2 done | Synthesis: codex | Total: 13m3s

@wesm

wesm commented Jul 30, 2026

Copy link
Copy Markdown
Member

will merge this pending CI

Windows materializes SQLite snapshots through CSV views instead of DuckDB's sqlite scanner. Schema detection selected source-native attribution for current archives, but the CSV projection omitted that column, causing every cache build against a migrated schema to fail at bind time.

Keep the fallback projection aligned with the detected message schema while retaining compatibility with older archives that only expose the effective attribution flag.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
@roborev-ci

roborev-ci Bot commented Jul 30, 2026

Copy link
Copy Markdown

roborev: Combined Review (eb4ecb5)

No issues found.


Reviewers: 2 done | Synthesis: codex | Total: 12m43s

@wesm
wesm merged commit a0c1207 into kenn-io:main Jul 30, 2026
18 checks passed
@wesm

wesm commented Jul 30, 2026

Copy link
Copy Markdown
Member

thank you!

@salmonumbrella

Copy link
Copy Markdown
Contributor Author

@wesm you're very welcome. thank you for merging!

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants