Commit Graph

30 Commits

Author SHA1 Message Date
nitin c862a2a43d Switch call recorder post-call transcription to Gladia with code switching (#23532)
## What

Switches the Call Recorder app's post-meeting transcription provider
from Recall.ai's built-in transcription (`recallai_async`) to Gladia
(`gladia_v2_async`), with code switching enabled so mixed-language calls
transcribe correctly.

## Changes

- `create_transcript` requests now send `provider: { gladia_v2_async: {
language_config: { code_switching: true } } }` instead of
`recallai_async` with `language_code: 'auto'`. Gladia auto-detects the
spoken language by default, and code switching re-detects it per
utterance for calls that mix languages.
- The provider payload is extracted into a
`RECALL_ASYNC_TRANSCRIPT_PROVIDER` constant so a future
provider-selection variable can slot in without touching the request
code.
- SETUP.md documents the new operational requirement: a Gladia API key
must be added in the Recall.ai dashboard (Transcription > Gladia) for
each region in use, otherwise transcripts fail.

<img width="2810" height="1656" alt="CleanShot 2026-07-30 at 15 00
27@2x"
src="https://github.com/user-attachments/assets/c702ab09-eea8-4c54-8e5a-4941951391c9"
/>

tested on twenty dev recall workspace 

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23532?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-30 10:42:14 +00:00
nitin 7b81d9ab83 Fix call-recorder REC badge rendering as empty boxes (#23415)
## What was wrong

The bot camera image draws a "REC" pill on top of the workspace logo.
The label used an SVG `<text>` element, and sharp resolves SVG text
through the host's fonts. The runtimes that execute app logic functions
ship no fonts, so every character fell back to an empty box: the badge
showed "▯▯▯" instead of "REC" in real meetings. It looked fine locally
because dev machines have fonts.

## The fix

Draw the label as vector outlines instead of text. "REC" is outlined
once from Inter SemiBold and stored as an SVG path constant, so the
badge renders the same on any host with no font lookup. The pill width
is derived from its contents instead of hardcoded, and tests fail if
`<text>` or `font-family` ever comes back.

<img width="2120" height="1191" alt="CleanShot 2026-07-28 at 19 16 46"
src="https://github.com/user-attachments/assets/c5fa0958-35ba-48da-be9c-a6af81ec2fa0"
/>


1 -- the bug on prod
2 -- how it looks when its not bugged on prod
3 -- this branches changes

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23415?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-28 14:08:39 +00:00
martmull 1f55234d0b fix(call-recorder): leave call when only recording bots remain (#23053)
## Problem

Fixes
[core-team-issues#2689](https://github.com/twentyhq/core-team-issues/issues/2689).

`everyone_left_timeout` only fires when the bot is the sole remaining
participant, and Recall counts other recording bots as participants. So
when several bots share a meeting, none of them sees itself as alone.

Recall does enable `bot_detection` by default, but it ships an empty
`matches` list, so the name-based check can never classify anyone. The
only detector that actually runs is the behavioural one, at its default
20 minute grace plus 10 minute timeout. A meeting left with only bots
therefore stays open for around 30 minutes, and two Twenty bots in the
same call never recognise each other at all.

This happens when several workspace members are invited to the same
meeting and each has the recorder preference on, or when third-party
notetakers stay behind after the humans leave.

## What this does

Sends a full `automatic_leave.bot_detection` block plus
`silence_detection`:

- **`using_participant_names`** — the configured recorder name, so
co-scheduled Twenty bots recognise each other, plus a list of common
notetakers. `timeout: 10`, which is Recall's enforced minimum; their
example config shows `5` and the API rejects it.
- **`using_participant_events`** — a participant that never speaks nor
shares screen is treated as a bot.
- **`silence_detection`** — Recall's documented example values
(`activate_after: 1200`, `timeout: 300`). Previously unset, so it fell
back to Recall's 20 + 60 minute default.

Both bot detectors activate 5 minutes after the **meeting start time**,
not 5 minutes after the bot joins. The bot joins early by a configurable
amount, so anchoring to join time spent the grace period before the
meeting existed — at a 10 minute early join, detection would have gone
live 5 minutes before the meeting began.

`everyone_left_timeout` is unchanged and still covers the ordinary case.

Effect:

| | before | after |
|---|---|---|
| Only bots remain | ~30 min | ~5 min after meeting start |
| Someone leaves the call open after talking | ~80 min | ~25 min |

## Deferred

De-duplicating bots per meeting URL, so several `callRecording`s in one
meeting share a single bot instead of each spawning one. `bot_detection`
is still needed for third-party bots, so this ships first.

---------

Co-authored-by: ehconitin <nitinkoche03@gmail.com>
2026-07-28 08:51:40 +00:00
martmull e0debf87a7 fix(call-recorder): listen proper updated fileds event (#23135)
## Context

Around meeting-end peaks (~6pm), Recall/Svix delivers event bursts for
every recorded call across the 700+ workspaces the app is installed on.
Each delivery was processed synchronously in the API request path, and
internal failures surfaced to Svix as non-2xx, so it redelivered — a
self-feeding storm of 500s and latency that only stopped when the
webhook endpoint was disabled.

## What changed

With #23134, server-route dispatch defaults to **queued** server-side:
the API acks Svix with a 202 right after signature verification,
`process-recall-webhook` runs on the worker queue, and failed runs retry
there (resolver `retryLimit`, default 3). The resolver needs no change
at all — `recall-webhook.ts` is back to main, and no SDK update is
required.

Remaining app changes:
- `schedule-recall-bot-on-call-recording-update` declares
`updatedFields` (the pending-transition fields) on its
`callRecording.updated` trigger, so the server drops the app's own
scheduling-progress and artifact writes **before** spawning a full
execution instead of executing and returning "skipped". The in-handler
check stays as a fallback.
- Version bumped to 1.5.0.
- Code comments introduced by earlier revisions of this PR removed per
review.

## Tests

- New test pins the trigger's `updatedFields` declaration.
- `yarn test:unit` (507 tests), `yarn lint`, `yarn typecheck` all green.

## Notes

- The `call-recorder (dockerhub-latest)` CI leg fails because main
already requires `twenty >= 2.23.0` while the latest published image is
2.22.0 — pre-existing, clears when 2.23.0 images publish.
- The 250s `import-call-recording-artifacts` route still runs in an API
request slot behind the fire-and-forget own-route POST; moving it fully
off the request path is a follow-up.
2026-07-22 14:58:25 +02:00
martmull a0e8d48656 Reduce call-recorder recovery crons to daily to relieve production (#23099)
## Context

Call Recorder is installed on 700+ workspaces and its two recovery crons
run every 15 minutes with the same pattern in every workspace, so all
executions land on the same minute boundaries and impact production. The
`callRecording.updated` event trigger (#23014) now covers the fast path
within seconds; these crons are only backstops for crashed creations and
missed webhooks.

## What changed

Pattern updates only, no logic changes:

- `process-pending-call-recording-requests`: `*/15 * * * *` -> `0 3 * *
*`
- `reconcile-stale-bot-state`: `*/15 * * * *` -> `30 3 * * *`

The daily times are staggered half an hour apart from the existing daily
crons (04:00 upcoming-events sweep, 04:30 orphaned-bots cleanup) so the
four daily jobs never coincide.

## Notes

- Recovery latency for rows missed by the event trigger becomes up to
24h instead of 15min, which is acceptable for backstops (the 7-day
convergence lookback is unaffected).
- Cron patterns live in installed manifests, so existing installations
pick this up on app upgrade only.
- The daily herd across workspaces at 03:00/03:30 remains synchronized
until generic cron spreading lands server-side (#23088 covers only the
`*/5` and `*/15` patterns).

---------

Co-authored-by: martmull <martin@twenty.com>
2026-07-21 12:11:06 +02:00
martmull 1b5e974629 feat(call-recorder): add copy-to-clipboard buttons for transcript, summary, and video link (#23052)
## Context

Closes twentyhq/core-team-issues#2692.

Adds copy-to-clipboard actions to the call recorder app so users can
quickly share a call's transcript, summary, and video.

## What changed

- **Copy transcript** button in the *Recording and Transcript* widget
header. Copies the transcript as plain text with resolved speaker
display names and timestamps (mirroring what is shown on screen).
- **Copy video download link** button in the same header. Copies the
signed video file URL.
- **Copy summary** button in the *Summary* widget header. Copies the
summary markdown.

Each button is powered by a new reusable `CopyToClipboardButton`
component that writes to the clipboard, briefly swaps to a check icon
for feedback, and surfaces a success/error snackbar. Buttons are
disabled when there is nothing to copy (no transcript / video / summary,
or while loading).

A `buildTranscriptPlainText` utility turns parsed transcript entries
into shareable text, with participant display names preferred over raw
diarized speaker labels.

## Screenshots

The *Recording and Transcript* header now shows a copy-transcript and a
copy-video-link button, and the *Summary* header shows a copy-summary
button.

| Light | Dark |
| --- | --- |
| <img width="426"
src="https://raw.githubusercontent.com/twentyhq/twenty/claude/issue-2692-screenshots/.github/pr-screenshots/2692/call-recorder-copy-buttons-light.png"
/> | <img width="426"
src="https://raw.githubusercontent.com/twentyhq/twenty/claude/issue-2692-screenshots/.github/pr-screenshots/2692/call-recorder-copy-buttons-dark.png"
/> |

## Tests

- New unit tests for `buildTranscriptPlainText` (speaker/timestamp
formatting, missing timestamps, participant name resolution).
- Full app unit suite passes (491 tests), plus typecheck and lint.
2026-07-20 15:40:17 +00:00
martmull fa720358d9 Ignore call-recorder bots for unsupported meeting platforms (#23050)
## What

The call-recorder scheduled a Recall bot for any calendar event that had
a conference link, even when the link pointed to a platform Recall
cannot join (e.g. ro.am, Daily, Whereby, or a plain dial-in). Those
requests could never produce a recording.

This adds a supported-platform check to the recording policy so
unsupported links are ignored, with a dedicated reason, and documents
the supported platforms in the app README.

## Changes

- Add `SUPPORTED_MEETING_PLATFORM_URL_PATTERNS` constant (Zoom, Google
Meet, Microsoft Teams, Webex, GoTo Meeting), extracted from the existing
link-extraction patterns so extraction and validation share one source
of truth.
- Add `isSupportedMeetingPlatformUrl` util.
- `resolveCallRecorderPolicyResult` now returns
`UNSUPPORTED_MEETING_PLATFORM` (bot not required) when the resolved
conference link is not a supported platform.
- Document supported platforms and the ignore behavior in the
call-recorder README.

## Tests

- New unit tests for `isSupportedMeetingPlatformUrl`.
- New policy test for the unsupported-platform case; updated existing
policy tests to use real supported URLs.
- All call-recorder unit tests pass; typecheck and lint clean.

Closes twentyhq/core-team-issues#2705


---
_Generated by [Claude
Code](https://claude.ai/code/session_01MWPkbdUg4QMdj4FM5mtNww)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23050?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-20 13:49:21 +02:00
martmull 89609c520c Reduce call-recorder Recall API load and harden bot scheduling recovery (#23014)
## Context

We receive Recall rate limit alerts on `/api/v1/bot`. Recall's List Bots
endpoint allows only 60 requests/min per Recall workspace (vs 300/min
for Retrieve and 120/min for Create), and that budget is shared by every
Twenty workspace on the instance since `RECALL_API_KEY` is a
server-level variable. The call-recorder recovery crons fanned out one
list call per stuck recording, fired at the same wall-clock minute for
every workspace, and never resolved dead rows, so the pending set only
grew.

This PR reworks the bot-scheduling recovery mechanism so that
crash-recovery work is rare, cheap, and mostly event-driven. One commit
per change:

## Changes

1. **Fail never-scheduled recordings once their meeting ends**
(`bot_never_scheduled` failure reason). Previously these rows stayed
`REQUESTED+SCHEDULED` forever and were re-fetched by every recovery run.
Rows with an unresolved creation attempt keep their recovery chance
until the 7-day convergence lookback passes (a bot may have recorded
before the id write-back was lost), then fail as
`bot_schedule_outcome_unknown`.

2. **Batch bot lookups into one list call per run.** The pending-bot
sweep and the failed-cancellation retry each issue at most one
workspace-wide `GET /api/v1/bot/` (filtered by `twentyWorkspaceId` +
active statuses) and match bots to recordings in memory via
`twentyCallRecordingId` metadata, instead of one list call per stuck
row. Truncated lists count as failed lookups so an incomplete map never
authorizes a duplicate creation.

3. **Record a `botScheduleAttemptedAt` marker before POSTing a bot.**
Recovery can now distinguish rows that never reached Recall (re-schedule
directly, zero Recall reads) from rows whose creation outcome is unknown
(only these join the lookup).

4. **Store the bot-creation `Idempotency-Key` on the row and recover by
re-sending.** When a stuck row's stored key still hashes from the
current scheduling inputs, recovery re-sends the creation: Recall either
returns the existing bot or creates the intended one, all on the Create
budget (120/min) without touching the List budget (60/min). Drifted
inputs still fall back to the lookup. Re-sends preserve the first
attempt's timestamp and are only trusted within a 12-hour window, so
repeated unknown outcomes age into the lookup path rather than risking a
twin bot after Recall's key retention expires.

5. **Resume pending rows on `callRecording.updated` events and slow the
cron.** A new database-event trigger resumes scheduling within seconds
when a row transitions back to pending (bot vanished at Recall, canceled
request re-requested, failed row reset by reconciliation), with queue
retries. It skips creations (the inserting run schedules inline), skips
its own progress writes, uses slim-payload diffs to skip cheaply, and
defers ambiguous rows to the cron so event bursts cannot fan out list
calls. The pending-requests cron becomes a backstop and drops from every
5 minutes to every 15.

Follow-up commits harden edge cases raised in review (status
revalidation before POST, per-row cancellation recovery window,
future-timestamp guard, attempt-state cleanup when a bot is confirmed
gone at Recall) and add a lifecycle integration test.

## Notes

- Two new app fields on `callRecording`: `botScheduleAttemptedAt`
(DATE_TIME) and `botScheduleIdempotencyKey` (TEXT), both nullable and
not UI-editable.
- A tight race between the event trigger and the cron converges on one
bot via the deterministic idempotency key.
- Not addressed here (needs a server-side change): per-workspace jitter
when dispatching logic-function cron triggers, so identical patterns
don't fire for every workspace on the same minute.

## Test

- New `call-recorder-lifecycle.integration-test.ts` on the app's
integration harness: the global setup installs the app on a live test
server, all reads and writes go through the real API into the test
database, and only externals are mocked — the Recall API (a fetch
interceptor that replays the same bot for a repeated `Idempotency-Key`,
like the real API) and the trigger transports (webhook payloads invoke
the webhook logic function handler; cron and database-event triggers run
their flows). Thirteen scenarios assert the resulting CallRecording rows
in the DB: scheduling from calendar reconciliation (events attached to a
seeded `SHARE_EVERYTHING` calendar channel, since unassociated events
are invisible), webhook status progression with artifact-import route
calls, transcript completion, out-of-order delivery protection, fatal
failure, unknown bots, cancellation with retried Recall delete, and
every crash recovery path. Verified locally against a live server: 15
integration tests pass (including the existing schema contract test).
- `yarn test:unit`: 488 tests pass. `yarn typecheck` and `yarn lint`
clean.

---------

Co-authored-by: martmull <martin@twenty.com>
2026-07-20 10:37:04 +02:00
martmull 4e2f9e3416 Fix join at in the past (#23000)
as title, we floor the bot join at date 1second in the future

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23000?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-17 14:59:09 +00:00
nitin 19f0e3cad5 Reduce Recall bot lifecycle reconciliation traffic (#22908)
## Summary

- split pending Call Recording request maintenance from stale recording
convergence
- recover Recall bots by workspace and Call Recording metadata before
creating a replacement
- retry failed cancellations, including the canceled-plus-botless
write-back race
- move the broad orphaned-bot list sweep from every five minutes to a
dedicated daily job
- filter Recall bot lists by workspace metadata at the provider boundary

## Why

This is stack 1/3 extracted from #22739. Healthy installed workspaces
currently list Recall bots every five minutes even when no local state
has diverged. This layer removes that unconditional list sweep while
keeping pending request recovery at five-minute latency.

The cancellation recovery also closes a crash window where Recall
accepted a bot creation but the local bot ID write-back failed before
the user canceled the request. The maintenance job now rediscovers and
cancels that bot before it can join.

## Stack

1. **Recall bot lifecycle reconciliation** — this PR
2. Divergence-scoped recording synchronization — #22909
3. Artifact import offloading — #22910

## Validation

- `npm run typecheck`
- `npm run lint`
- `npm run test:unit` — 71 files, 454 tests

---------

Co-authored-by: Claude <martmull@hotmail.fr>
2026-07-17 15:49:28 +02:00
nitin 79f3a5243a Add callAppRoute to RestApiClient (#22863)
Adds a `callAppRoute` method to `RestApiClient` in
`twenty-client-sdk/rest`. It calls one of the app's own HTTP routes
using the injected `TWENTY_FUNCTIONS_URL`, resolved internally the same
way the client already resolves `TWENTY_API_URL`, so app code no longer
reads env vars or knows how function routes are hosted.

Both app runtimes already go through `RestApiClient` for route calls
(logic functions and front components), so both get this in one place;
front components keep the existing 401 token-refresh flow.

Pairs with #22825, which makes the injected `TWENTY_FUNCTIONS_URL`
callable in every topology (app custom domain -> workspace isolated
functions domain -> `SERVER_URL/s`).

Once this ships in an SDK release, Call Recorder's own-route plumbing
(logic-function and front-component utils) drops its URL resolution and
calls `client.callAppRoute(path, body)`.

---------

Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-07-15 18:07:16 +00:00
nitin cdb2590355 Migrate call-recorder tests off own-code vi.mock (#22902)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22902?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-15 16:56:26 +02:00
nitin 2201917f33 Harden call recorder Recall API boundary (#22832)
Part 2/5 of splitting #22739. Stacked on #22831.

- `RecallBotSnapshot`: Recall bot payloads are parsed once at the API
boundary (`parseRecallBotSnapshot`);
`getRecallBot`/`listScheduledRecallBots` return typed snapshots, flows
never touch raw provider records
- Retry policy: honors `Retry-After` (seconds or HTTP-date, capped at
60s), treats 409 and 507 (ad-hoc pool exhausted) as retryable with
tailored delays, adds equal jitter to the linear backoff, and returns
instead of sleeping past 10s in-process so invocations never sleep into
their timeout
- `listScheduledRecallBots` accepts a server-side `metadata__` filter
and reports `truncated` instead of failing beyond 10 pages
- Extracts `cancelOrEjectRecallBot` into the recall-api layer
- Replaces the `CALL_RECORDER_MAX_MEDIA_FILE_SIZE_MB` server variable
with a fixed 500 MB constant: uploads stream since #22652, so the cap no
longer guards function memory and does not need to be operator-tunable

Next: billing charge verification, divergence-scoped sync crons, webhook
artifact continuation.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22832?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-15 15:20:18 +05:30
nitin f13bde1e03 Align call recorder vocabulary with core dialect (#22831)
Part 1/5 of splitting #22739 into a reviewable stack.

Mechanical renames only, no behavior change:
- `ingestion` -> `import` across data/domain/flows
(`completeCallRecordingIngestion` -> `completeCallRecordingImport`,
`ingestCallRecordingMedia` -> `importCallRecordingMedia`,
`reconcileCallRecordingTranscriptArtifact` ->
`importCallRecordingTranscript`, ...)
- `reapOrphanedCallRecorders` -> `cleanupOrphanedRecallBots`
- `ensureCallRecorder` -> `scheduleRecallBotForCallRecording`,
`healCallRecordingsMissingBot` ->
`scheduleRecallBotsForPendingCallRecordings`
- `extractRecallBotConvergence` -> `extractRecallBotSyncState`
- formatting drift in touched files

Next in the stack: Recall API hardening, billing charge verification,
divergence-scoped sync crons, webhook artifact continuation.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22831?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-14 01:51:39 +05:30
nitin 78a0f9ea77 feat(call-recorder): type application and server variables, make summary prompt rich text (#22685) 2026-07-08 22:39:47 +02:00
nitin 640e6b8b33 fix(call-recorder): stream Recall media to storage to fix OOM (#22652)
## Summary

Fixes Call Recorder media ingestion OOMs by streaming Recall media into
Twenty direct uploads instead of buffering the full file in memory.

## Changes

- Opens the Recall media download stream and uses its `Content-Length`
as the direct upload size.
- Creates a Twenty direct upload target, streams the media body to it
with Node `http`/`https` backpressure, then completes the upload.
- Cleans up download/upload streams on target creation, upload, and
storage response failures.
- Keeps the media size cap for now while making it no longer required
for memory safety.
- Bumps `twenty-client-sdk` and `twenty-sdk` to `2.19.0`.

## Tests

- `yarn test:unit
src/logic-functions/flows/__tests__/ingest-call-recording-media.test.ts
src/logic-functions/flows/__tests__/put-media-download-body-to-upload-target.test.ts`
- `yarn typecheck`
2026-07-08 15:48:49 +02:00
martmull 435073e9c5 Display featured applications in marketplace (#22635)
## After
<img width="1060" height="589" alt="image"
src="https://github.com/user-attachments/assets/74dfadcf-8698-4404-81c6-b309cc4cbf79"
/>
<img width="732" alt="image"
src="https://github.com/user-attachments/assets/0e1a3644-04bc-4208-aa77-3842d9db9cc8"
/>
<img width="797" alt="image"
src="https://github.com/user-attachments/assets/0456ecce-607a-4705-8a89-c77029bfb6ac"
/>

- Remove IS_MARKETPLACE_SETTING_TAB_VISIBLE feature flag
- add vetted toggle in admin app tab
- added people data labs, last contact and call recorder to default
vetted applications

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22635?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-07-07 17:05:54 +02:00
nitin 18c10f4632 Call recorder: sweep upcoming calendar events for recording bots on install (#22552)
## Context

The Call Recorder schedules Recall bots reactively — a database event
trigger reconciles a calendar event when it is created or updated. That
misses meetings that already existed before the app was installed, and
meetings created far ahead that are never edited as they approach.
Neither gets a bot, though recording is on by default.

## What this PR does

Moves to the rolling near-term window Recall recommends for [your own
calendar
integration](https://docs.recall.ai/docs/creating-and-scheduling-bots#scheduling-bots-with-your-own-calendar-integration)
— "a daily sync of the next 7 days". Bots are scheduled only for
meetings starting within a **7-day horizon**, kept complete by three
mechanisms:

- **Horizon (policy).** `resolveCallRecorderPolicyResult` caps
scheduling at 7 days from now (`EVENT_BEYOND_SCHEDULING_HORIZON`),
measured from `startsAt` (the bot's join time). The existing reactive
trigger inherits this — far-future creates no longer schedule, and a
meeting moved out of the window has its bot canceled.
- **Daily sweep (cron).** New `sweep-upcoming-calendar-events`
reconciles the 7-day window each day, so a meeting that ages into it
without being edited still gets a bot.
- **Fresh-install seed (post-install).** The app's single post-install
hook (`start-post-install-backfills`) runs the sweep once on a fresh
install so a new workspace is covered right away instead of waiting for
the first cron; on an upgrade it relies on the cron and backfills
missing summaries instead.

The sweep runs through the authenticated
`reconcile-upcoming-calendar-events` route, which batches ids through
the existing reconciliation flow and re-invokes itself near the 900s
timeout. Deterministic recording ids keep it idempotent. App self-calls
go through a shared `postToOwnRoute` util targeting the server-injected
`TWENTY_FUNCTIONS_URL`; a failed kickoff throws so the async hook
retries instead of going silently green.

Also: fallback titles for call recordings whose calendar event is
visibility-restricted; app version → 1.0.7.

## Deferred

- Far-future bots already scheduled by the previous no-cap behavior
aren't proactively canceled — they fire naturally, or cancel if their
event is edited out of the window.
- Recall rejects an in-place `join_at` update under 10 min out; today
that logs a warning rather than delete-and-recreate.

## Test plan

- `yarn test:unit`: 407 tests / 65 files pass — new coverage for the
horizon (including a meeting that starts in-window but ends beyond it),
the 7-day query filter, the cron handler, the post-install hook's
fresh-install vs upgrade branches, and the batch/continuation flow.
- `yarn typecheck`, `yarn lint`, and `yarn twenty dev:build` (manifest
build) pass.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22552?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-07 19:03:28 +05:30
Paul Rastoin fe442fe5fe chore(apps): bump sdk to 2.19.0-alpha.1 and require twenty server >=2.19.0 (#22601)
## Summary

- Bumps `twenty-sdk` / `twenty-client-sdk` to the exact `2.19.0-alpha.1`
prerelease for the apps under `packages/twenty-apps` that actually
target a mutated standard identifier, and refreshes their lockfiles.
- Declares `"engines": { "twenty": ">=2.19.0" }` in those apps so
pre-2.19 servers refuse to install or upgrade to the rebuilt packages.

Only apps that reference a standard object's **system-field** universal
identifier, define a **relation into** a standard object, or call the
field-UID derivation helper need 2.19 (the identifiers those touch
changed from hardcoded UUIDs to deterministic hashes). Apps that only
define their own custom objects, or add plain scalar fields to a
standard object via its stable object-level id, were left on their prior
SDK pins. Currently bumped: `postcard`, `self-hosting`,
`twenty-partners`, `call-recorder`, `people-data-labs`,
`twenty-last-contact`.

## Context

Follow-up to #22565 (deterministic system field universal identifiers)
and #22599 (SDK prerelease bump).

Packages built with SDK ≤ 2.18 carry legacy system field identifiers and
are rejected by servers running `main`. Rebuilding with the 2.19 SDK
fixes that — but a rebuilt package must not be *upgraded into* by a 2.18
server, since 2.18 has no deterministic-identifier validation and would
diff the changed system field identifiers as a destructive delete +
create (the 2.19 backfill has not run there yet).

The `engines.twenty` constraint closes that gap: `doInstallApplication`
validates it via `validateServerCompatibility` before any mutation, and
this check has shipped since ~2.10, so every 2.18 server enforces it.
Resulting matrix:

- 2.18 fresh install of a rebuilt app: works, converges as a no-op once
the 2.19 backfill runs
- 2.18 upgrade of an existing install: rejected with
`SERVER_VERSION_INCOMPATIBLE` before any mutation
- 2.19 (post-backfill) install/upgrade: syncs cleanly

## Expected CI failures

**The `CI Twenty Apps` integration-test jobs are expected to fail on
this PR** (e.g. `people-data-labs`, `twenty-partners`). This is a
server-version mismatch, not an app bug — lint, typecheck and unit tests
all pass:

- The integration step spawns a real Twenty server from Docker Hub
`twentycrm/twenty-app-dev:latest` and runs `twenty dev` to sync each
app's metadata into it.
- `latest` currently resolves to **v2.18.5** — no `2.19` image is
published to Docker Hub yet.
- These apps now reference 2.19's **deterministic system-field universal
identifiers** (e.g. `company.createdBy`, `opportunity.createdAt`). A
2.18 server still carries the legacy identifiers, so the sync rejects
every 2.19-derived reference with `INVALID_VIEW_DATA` /
`FIELD_METADATA_NOT_FOUND` ("Field metadata not found").
- The failure surfaces as low-level field errors rather than a clean
`SERVER_VERSION_INCOMPATIBLE` because the `engines.twenty` gate
(`validateServerCompatibility`) only runs on the `app:install` / publish
paths — **not** on the `twenty dev` dev-sync path the integration tests
use.

These jobs will go green automatically once `twenty-app-dev:2.19` is
published to Docker Hub (or once CI pins the spawn action's
`twenty-version` to a 2.19 tag).

## Intentionally not included

- App `version` fields are untouched; they'll be bumped at publish time.

## Test plan

- [x] Refresh each bumped app's `yarn.lock` (`2.19.0-alpha.1` is now on
npm)
- [ ] Rebuild one app manifest and verify default field identifiers
match `getFieldUniversalIdentifier`
- [ ] Verify a 2.18 server rejects an upgrade to a rebuilt package with
`SERVER_VERSION_INCOMPATIBLE`
- [ ] Re-run `CI Twenty Apps` integration jobs once a
`twenty-app-dev:2.19` image is available

Made with [Cursor](https://cursor.com)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22601?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-light.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-07 13:03:43 +02:00
nitin cabe5545ae Use SDK calendarEventRecordPageFields identifiers in call-recorder (#22618)
Replaces the hardcoded calendarEventRecordPageFields view/group
identifiers in the call-recorder preference view-field with
`STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.calendarEvent.views.calendarEventRecordPageFields`,
resolving the TODO. The published `twenty-sdk@2.18.0` (already pinned by
the app) ships these identifiers with values matching the previously
hardcoded ones.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22618?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-07 15:32:18 +05:30
nitin fe8ec0463a Call recorder: parse conference links from calendar event text (#22555) 2026-07-05 17:44:13 +02:00
nitin badaae17cd Call recorder: skeleton loading state + port front components to twenty-ui (#22473)
## What this does

Three changes to the call-recorder app's recording/transcript front
component:

### 1. Fix loading flicker with a skeleton loader (`4dc3a167`)

The widget's loading state rendered an empty `<video controls>` element
at 16:9, which was torn down and replaced once the recording query
resolved — so every open flashed a video player, even for calendar
events with no video at all.

- New `RecordingSkeletonLoader`: three transcript-shaped pulsing rows
(avatar circle + speaker bar + text bars) held at the same 240px
min-height as the empty state, so nothing jumps when content arrives.
Visuals follow the twenty-front skeleton standard
(react-loading-skeleton theming: `background.tertiary` base,
`background.transparent.lighter` highlight sweep, 4px radius), rebuilt
with emotion since the sandboxed bundle can't import the host's global
skeleton stylesheet.
- `RecordingVideoPlayer` now only renders with a real URL (`src:
string`).

### 2. Port front components to twenty-ui (`cea0d6ee`)

`twenty-ui@1.0.0-alpha.1` is now published and consumable by apps
(CSS-injection build support + the renderer's style bridge), so the
"remove once twenty-ui can be imported safely" duplications are
resolved:

- Deleted `recording-theme-css-variables.ts` → all components use
`themeCssVariables` from `twenty-ui/theme-constants` (only non-1:1
rename: `accent.primary` → `accent.accent9`).
- Deleted `TranscriptSpeakerAvatar` / `TranscriptSpeakerChip` → replaced
with twenty-ui `Avatar` (`size="md"`, `type="rounded"`) and `Chip`
(`ChipVariant.Transparent`, `isBold`, non-clickable) in
`TranscriptEntryListItem`.
- Added `import 'twenty-ui/style.css'` to the front-component entry,
plus a root `css.d.ts` declaration. The import is load-bearing: the SDK
inlines the CSS into the component bundle and the renderer's style
bridge injects it into the host document — the host's own twenty-ui CSS
can't be relied on, since scoped class hashes are content-derived and
drift between builds.
- Bumped `twenty-sdk` / `twenty-client-sdk` to `^2.18.0` (needed for the
CSS-injection build support) and added `twenty-ui@^1.0.0-alpha.1`.

### 3. Fix manifest build + typecheck after the port (`baa6eb2f`)

- `yarn twenty dev --once` failed at "Building manifest..." — the
manifest build loads front-component modules with `twenty-ui`
proxy-mocked, so the named `themeCssVariables` import resolves to
`undefined` and any static `${themeCssVariables.x}` interpolation throws
at module scope. All static interpolations are now lazy (`${() =>
themeCssVariables.x}`), deferring the access to render time in the
browser.
- Added `css.d.ts` to `tsconfig.spec.json`'s `include` — its
`["src/**"]` include replaced the parent config's default file set, so
the `*.css` module declaration was never loaded and `yarn typecheck`
failed with TS2882.

## Notes for review / testing

- Worth exercising manually: the skeleton during load, speaker
chips/avatars rendering via twenty-ui, and hovering a long (overflowing)
speaker name — Chip's tooltip uses a react-dom portal, which is the one
path not previously exercised inside the front-component sandbox.
- App version intentionally left at `1.0.4`.

https://claude.ai/code/session_01WuHQFWiRocn82ejxsnmz28

---
_Generated by [Claude
Code](https://claude.ai/code/session_01WuHQFWiRocn82ejxsnmz28)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22473?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-03 23:10:28 +05:30
nitin cbbb11b774 Add configurable call recorder summaries (#22405)
<img width="2560" height="1319" alt="CleanShot 2026-07-02 at 16 58 03"
src="https://github.com/user-attachments/assets/d968bff7-4b57-4816-be9c-02e5af32ae3d"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22405?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-03 21:48:48 +05:30
nitin bc1a4cb3fb feat(call-recorder): cap media file size during ingestion to avoid OOM (#22463)
Media ingestion buffered whole Recall files in memory; long recordings
OOM'd the logic function.

- Caps the size of ingested media files, configurable via the
`CALL_RECORDER_MAX_MEDIA_FILE_SIZE_MB` app variable (default 80 MB).
- Downloads stream chunk by chunk and stop at the cap; a Content-Length
above the cap skips the download without reading the body.
- A skipped file is recorded as `video_file_too_large` /
`audio_file_too_large` in `callRecorderFailureReason`; the completion
gate treats a marked file as resolved, so the recording still completes
and bills with its remaining artifacts.
- A real failure reason always wins over the size markers when the
recording fails.

Deferred: the cap is a stopgap until core supports streaming uploads
(TODO in code).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22463?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-02 20:55:47 +05:30
nitin 4045f9852e feat(call-recorder): use workspace logo for Recall bot image (#22302)
workspace logo - 

<img width="433" height="465" alt="CleanShot 2026-06-29 at 20 43 11"
src="https://github.com/user-attachments/assets/946782a5-7bab-461b-a613-ce056fec8bbc"
/>


how bot appears - 

<img width="2560" height="1315" alt="CleanShot 2026-06-29 at 20 47 01"
src="https://github.com/user-attachments/assets/91daa29c-83ef-4ac1-9d7c-ffa8bcf08135"
/>



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22302?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-30 14:01:15 +05:30
nitin f263edd122 Add call recorder oom diagnosis logs (#22315)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22315?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-29 17:14:06 +02:00
martmull 1bf0d06e18 Add author to twenty apps (#22226)
as title

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22226?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-26 15:11:37 +00:00
nitin 4f429565e1 call recorder polishes (#22170)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22170?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 18:27:43 +05:30
nitin db687c7407 chore(call-recorder): drop FAILED status schema bridge (#22134)
Cleanup of the call-recorder data layer + the SDK 2.16 bump and its
fallout, now that the `FAILED_UNKNOWN → FAILED` rename shipped in
`twenty/v2.16.0` (#22062).

- **Drop the schema bridge.** `executeCurrentSchemaMutation` and the
integration-test compatibility filter existed only to work around
servers exposing `FAILED_UNKNOWN`. Deleted the bridge;
`updateCallRecording` / `completeCallRecordingIngestion` call
`client.mutation(...)` directly. Integration test iterates all
`CallRecordingStatus` values.
- **Bump SDK** `twenty-sdk` / `twenty-client-sdk` to `2.16.0`.
- **One export per file.** Move `CallRecordingUpdateFields` to its own
type file; extract the duplicated media-file shape into
`CallRecordingMediaFile`.
- **Migrate the Recall webhook to `serverRouteTriggerSettings`** (2.16
dropped `serverWebhookTriggerSettings` + its declarative
`workspaceIdResolver`). The webhook is now a **resolver**
(`recall-webhook`) that verifies the Svix signature, reads
`twentyWorkspaceId` from the Recall bot metadata, and returns `{
workspaceId, targetLogicFunctionUniversalIdentifier, payload }`; the
platform dispatches to a new **target** function
(`process-recall-webhook`) in the resolved workspace, where
`CoreApiClient` is workspace-scoped. Resolver UID/route unchanged, so
the registered Recall endpoint URL stays valid. Failures now throw →
HTTP 500 (Svix retries) instead of returning 401/400.

Verified: typecheck, 213 unit tests, oxlint, oxfmt all green. **Not yet
verified end-to-end against a live server** — call-recorder is the first
app on `serverRouteTriggerSettings`, so a real Recall webhook should be
tested through the resolver→target path before relying on it.
2026-06-25 11:06:58 +02:00
nitin 811fecc119 chore(apps): move call-recorder to public, remove stale meeting-bot duplicate (#22132)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22132?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 00:13:13 +05:30