Skip to content

Commit ec42b6f

Browse files
committed
Merge branch 'main' into ehco/call-recording-converge-on-meeting-start
2 parents 298fd55 + e7e9924 commit ec42b6f

1,175 files changed

Lines changed: 119675 additions & 1704 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"packages/twenty-utils",
7575
"packages/twenty-zapier",
7676
"packages/twenty-website",
77+
"packages/twenty-website-redone",
7778
"packages/twenty-docs",
7879
"packages/twenty-e2e-testing",
7980
"packages/twenty-shared",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { type TranscriptMarker } from 'src/logic-functions/types/transcript-marker.type';
2+
3+
export const buildPendingTranscriptMarker = ({
4+
recallTranscriptId,
5+
requestedAt,
6+
}: {
7+
recallTranscriptId: string;
8+
requestedAt: string;
9+
}): TranscriptMarker => ({
10+
recallTranscriptId,
11+
status: 'PENDING',
12+
requestedAt,
13+
});

packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/__tests__/converge-diverged-call-recordings.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,15 +163,15 @@ describe('convergeDivergedCallRecordings', () => {
163163
callRecordingId: 'call-recording-1',
164164
});
165165
expect(client.mutations).toEqual([
166-
{
166+
expect.objectContaining({
167167
id: 'call-recording-1',
168-
data: {
168+
data: expect.objectContaining({
169169
status: 'PROCESSING',
170170
startedAt: '2026-06-09T13:02:00.000Z',
171171
endedAt: '2026-06-09T14:00:00.000Z',
172172
externalRecordingId: 'recall-recording-1',
173-
},
174-
},
173+
}),
174+
}),
175175
]);
176176
expect(chargeCompletedCallRecordingMock).not.toHaveBeenCalled();
177177
expect(result).toEqual({
@@ -470,6 +470,11 @@ describe('convergeDivergedCallRecordings', () => {
470470
id: 'call-recording-1',
471471
data: {
472472
endedAt: '2026-06-09T14:00:00.000Z',
473+
transcript: {
474+
recallTranscriptId: 'recall-transcript-1',
475+
status: 'PENDING',
476+
requestedAt: NOW.toISOString(),
477+
},
473478
},
474479
},
475480
]);

packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/__tests__/handle-recall-webhook.test.ts

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -750,25 +750,17 @@ describe('handleRecallWebhook', () => {
750750
status: 'PROCESSING',
751751
externalBotId: 'recall-bot-1',
752752
externalRecordingId: 'recall-recording-1',
753+
transcript: {
754+
recallTranscriptId: 'recall-transcript-1',
755+
status: 'PENDING',
756+
requestedAt: expect.any(String),
757+
},
753758
},
754759
},
755760
]);
756761
});
757762

758-
it('does not re-request a transcript on a redelivered done event', async () => {
759-
listRecallTranscriptsMock.mockResolvedValue({
760-
ok: true,
761-
transcripts: [
762-
{
763-
id: 'recall-transcript-1',
764-
createdAt: '2026-01-01T14:06:00.000Z',
765-
downloadUrl: undefined,
766-
provider: 'recallai_async',
767-
statusCode: 'processing',
768-
statusSubCode: undefined,
769-
},
770-
],
771-
});
763+
it('does not re-request a transcript on a redelivered done event while Recall list is stale', async () => {
772764
const client = new FakeCoreApiClient([
773765
{
774766
id: 'call-recording-1',
@@ -800,6 +792,9 @@ describe('handleRecallWebhook', () => {
800792
expect(listRecallTranscriptsMock).toHaveBeenCalledWith({
801793
externalRecordingId: 'recall-recording-1',
802794
});
795+
expect(retrieveRecallTranscriptMock).toHaveBeenCalledWith({
796+
transcriptId: 'recall-transcript-1',
797+
});
803798
expect(client.mutations).toEqual([
804799
{
805800
id: 'call-recording-1',
@@ -858,14 +853,14 @@ describe('handleRecallWebhook', () => {
858853
callRecordingId: 'call-recording-1',
859854
});
860855
expect(client.mutations).toEqual([
861-
{
856+
expect.objectContaining({
862857
id: 'call-recording-1',
863-
data: {
858+
data: expect.objectContaining({
864859
status: 'PROCESSING',
865860
externalBotId: 'recall-bot-1',
866861
externalRecordingId: 'recall-recording-9',
867-
},
868-
},
862+
}),
863+
}),
869864
]);
870865
});
871866

@@ -972,15 +967,15 @@ describe('handleRecallWebhook', () => {
972967
callRecordingId: 'call-recording-1',
973968
});
974969
expect(client.mutations).toEqual([
975-
{
970+
expect.objectContaining({
976971
id: 'call-recording-1',
977-
data: {
972+
data: expect.objectContaining({
978973
status: 'PROCESSING',
979974
externalBotId: 'recall-bot-1',
980975
externalRecordingId: 'recall-recording-1',
981976
audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }],
982-
},
983-
},
977+
}),
978+
}),
984979
]);
985980
expect(chargeCompletedCallRecordingMock).not.toHaveBeenCalled();
986981
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export type ConvergeDivergedCallRecordingsResult = {
2+
candidateCount: number;
3+
updatedCallRecordingIds: string[];
4+
markedFailedCallRecordingIds: string[];
5+
requestedTranscriptCallRecordingIds: string[];
6+
unconvergeableCallRecordingIds: string[];
7+
skippedNotStartedCallRecordingIds: string[];
8+
};

packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/converge-diverged-call-recordings.util.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { isCallRecordingStatusDowngrade } from 'src/logic-functions/domain/is-ca
1919
import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util';
2020
import { persistCallRecordingProgress } from 'src/logic-functions/flows/persist-call-recording-progress.util';
2121
import { reconcileCallRecordingTranscriptArtifact } from 'src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util';
22+
import { type ConvergeDivergedCallRecordingsResult } from 'src/logic-functions/flows/converge-diverged-call-recordings-result.type';
2223
import { shouldCompleteCallRecordingIngestion } from 'src/logic-functions/domain/should-complete-call-recording-ingestion.util';
2324
import {
2425
updateCallRecording,
@@ -63,15 +64,6 @@ type DivergedCallRecordingNode = {
6364
calendarEvent?: { startsAt?: string | null; endsAt?: string | null } | null;
6465
};
6566

66-
export type ConvergeDivergedCallRecordingsResult = {
67-
candidateCount: number;
68-
updatedCallRecordingIds: string[];
69-
markedFailedCallRecordingIds: string[];
70-
requestedTranscriptCallRecordingIds: string[];
71-
unconvergeableCallRecordingIds: string[];
72-
skippedNotStartedCallRecordingIds: string[];
73-
};
74-
7567
// Webhook deliveries get lost; this pull pass re-derives state from Recall.
7668
export const convergeDivergedCallRecordings = async ({
7769
client,
@@ -119,6 +111,7 @@ export const convergeDivergedCallRecordings = async ({
119111
client,
120112
candidate,
121113
externalBotId: candidate.externalBotId,
114+
now,
122115
result,
123116
});
124117
}
@@ -229,11 +222,13 @@ const convergeCallRecording = async ({
229222
client,
230223
candidate,
231224
externalBotId,
225+
now,
232226
result,
233227
}: {
234228
client: CoreApiClient;
235229
candidate: DivergedCallRecordingCandidate;
236230
externalBotId: string;
231+
now: Date;
237232
result: ConvergeDivergedCallRecordingsResult;
238233
}): Promise<void> => {
239234
const botResult = await getRecallBot({ externalBotId });
@@ -269,6 +264,7 @@ const convergeCallRecording = async ({
269264
callRecordingId: candidate.id,
270265
currentStatus: candidate.status,
271266
externalRecordingId,
267+
requestedAt: now.toISOString(),
272268
transcript: candidate.transcript,
273269
});
274270

packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/handle-recall-webhook.util.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ const buildTranscriptArtifactUpdate = async ({
353353
callRecordingId: callRecording.id,
354354
currentStatus: callRecording.status,
355355
externalRecordingId,
356+
requestedAt: new Date().toISOString(),
356357
transcript: callRecording.transcript,
357358
});
358359

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { type CallRecordingUpdateFields } from 'src/logic-functions/data/update-call-recording.util';
2+
3+
type CallRecordingTranscriptArtifactUpdateFields = Pick<
4+
CallRecordingUpdateFields,
5+
'status' | 'transcript'
6+
>;
7+
8+
export type ReconcileCallRecordingTranscriptArtifactResult = {
9+
updateData: CallRecordingTranscriptArtifactUpdateFields;
10+
requestedTranscript: boolean;
11+
};

packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util.ts

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,32 @@
11
import { isNull, isUndefined } from '@sniptt/guards';
22

33
import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status';
4-
import { type CallRecordingUpdateFields } from 'src/logic-functions/data/update-call-recording.util';
54
import { buildFailedTranscriptMarker } from 'src/logic-functions/domain/build-failed-transcript-marker.util';
5+
import { buildPendingTranscriptMarker } from 'src/logic-functions/domain/build-pending-transcript-marker.util';
66
import { isCallRecordingStatusDowngrade } from 'src/logic-functions/domain/is-call-recording-status-downgrade.util';
77
import { parseTranscriptMarker } from 'src/logic-functions/domain/parse-transcript-marker.util';
88
import { createAsyncRecallTranscript } from 'src/logic-functions/recall-api/create-async-recall-transcript.util';
99
import {
1010
listRecallTranscripts,
11-
type RecallTranscriptSummary,
1211
} from 'src/logic-functions/recall-api/list-recall-transcripts.util';
12+
import { type RecallTranscriptSummary } from 'src/logic-functions/recall-api/recall-transcript-summary.type';
1313
import { downloadTranscript } from 'src/logic-functions/flows/download-transcript.util';
14+
import { type ReconcileCallRecordingTranscriptArtifactResult } from 'src/logic-functions/flows/reconcile-call-recording-transcript-artifact-result.type';
1415

15-
type CallRecordingTranscriptArtifactUpdateFields = Pick<
16-
CallRecordingUpdateFields,
17-
'status' | 'transcript'
18-
>;
19-
20-
export type ReconcileCallRecordingTranscriptArtifactResult = {
21-
updateData: CallRecordingTranscriptArtifactUpdateFields;
22-
requestedTranscript: boolean;
23-
};
16+
type CallRecordingTranscriptArtifactUpdateFields =
17+
ReconcileCallRecordingTranscriptArtifactResult['updateData'];
2418

2519
export const reconcileCallRecordingTranscriptArtifact = async ({
2620
callRecordingId,
2721
currentStatus,
2822
externalRecordingId,
23+
requestedAt,
2924
transcript,
3025
}: {
3126
callRecordingId: string;
3227
currentStatus: string | undefined;
3328
externalRecordingId: string;
29+
requestedAt: string;
3430
transcript: unknown;
3531
}): Promise<ReconcileCallRecordingTranscriptArtifactResult> => {
3632
const existingTranscriptMarker = parseTranscriptMarker(transcript);
@@ -60,8 +56,17 @@ export const reconcileCallRecordingTranscriptArtifact = async ({
6056
const transcriptArtifact = selectRecallTranscriptArtifact(
6157
listResult.transcripts,
6258
);
59+
const pendingTranscriptMarkerRecallTranscriptId =
60+
existingTranscriptMarker?.status === 'PENDING'
61+
? (existingTranscriptMarker.recallTranscriptId ?? undefined)
62+
: undefined;
63+
const transcriptIdToDownload =
64+
transcriptArtifact?.id ?? pendingTranscriptMarkerRecallTranscriptId;
6365

64-
if (isUndefined(transcriptArtifact)) {
66+
if (
67+
isUndefined(transcriptArtifact) &&
68+
isUndefined(pendingTranscriptMarkerRecallTranscriptId)
69+
) {
6570
const createResult = await createAsyncRecallTranscript({
6671
externalRecordingId,
6772
callRecordingId,
@@ -75,12 +80,21 @@ export const reconcileCallRecordingTranscriptArtifact = async ({
7580
return buildEmptyTranscriptArtifactResult();
7681
}
7782

78-
return { updateData: {}, requestedTranscript: true };
83+
return {
84+
updateData: {
85+
transcript: buildPendingTranscriptMarker({
86+
recallTranscriptId: createResult.transcriptId,
87+
requestedAt,
88+
}),
89+
},
90+
requestedTranscript: true,
91+
};
7992
}
8093

8194
if (
82-
transcriptArtifact.statusCode === 'failed' ||
83-
transcriptArtifact.statusCode === 'error'
95+
!isUndefined(transcriptArtifact) &&
96+
(transcriptArtifact.statusCode === 'failed' ||
97+
transcriptArtifact.statusCode === 'error')
8498
) {
8599
return {
86100
updateData: buildTranscriptFailureUpdate({
@@ -92,12 +106,19 @@ export const reconcileCallRecordingTranscriptArtifact = async ({
92106
};
93107
}
94108

95-
if (transcriptArtifact.statusCode !== 'done') {
109+
if (
110+
!isUndefined(transcriptArtifact) &&
111+
transcriptArtifact.statusCode !== 'done'
112+
) {
113+
return buildEmptyTranscriptArtifactResult();
114+
}
115+
116+
if (isUndefined(transcriptIdToDownload)) {
96117
return buildEmptyTranscriptArtifactResult();
97118
}
98119

99120
const downloadResult = await downloadTranscript({
100-
transcriptId: transcriptArtifact.id,
121+
transcriptId: transcriptIdToDownload,
101122
});
102123

103124
if (downloadResult.outcome === 'filled') {
@@ -113,7 +134,7 @@ export const reconcileCallRecordingTranscriptArtifact = async ({
113134
return {
114135
updateData: buildTranscriptFailureUpdate({
115136
currentStatus,
116-
transcriptId: transcriptArtifact.id,
137+
transcriptId: transcriptIdToDownload,
117138
subCode: downloadResult.subCode,
118139
}),
119140
requestedTranscript: false,

packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/recall-api/list-recall-transcripts.util.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,7 @@ import { asRecord } from 'src/logic-functions/utils/as-record.util';
55
import { getString } from 'src/logic-functions/utils/get-string.util';
66
import { getRecallApiConfig } from 'src/logic-functions/recall-api/get-recall-api-config.util';
77
import { recallBotApiRequest } from 'src/logic-functions/recall-api/recall-bot-api-request.util';
8-
9-
export type RecallTranscriptSummary = {
10-
id: string;
11-
createdAt: string | undefined;
12-
downloadUrl: string | undefined;
13-
provider: string | undefined;
14-
statusCode: string | undefined;
15-
statusSubCode: string | undefined;
16-
};
8+
import { type RecallTranscriptSummary } from 'src/logic-functions/recall-api/recall-transcript-summary.type';
179

1810
type ListRecallTranscriptsResult =
1911
| { ok: true; transcripts: RecallTranscriptSummary[] }

0 commit comments

Comments
 (0)