Fix AI chat streaming persistence when command menu closes (#14854)

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


- Removed `messageId` column from `File` table and its references in
code (unrelated to this PR)
- Updated AI chat to use React Context API with persistent provider in
`CommandMenuContainer`
- Converted all AI chat component states to regular Recoil atoms (no
instance context needed)

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Abdul Rahman
2025-10-03 16:07:41 +05:30
committed by GitHub
parent 329af2b670
commit 0648dc5c65
41 changed files with 385 additions and 415 deletions
@@ -1,32 +0,0 @@
import { Command, CommandRunner } from 'nest-commander';
import {
CLEANUP_ORPHANED_FILES_CRON_PATTERN,
CleanupOrphanedFilesCronJob,
} from 'src/engine/core-modules/file/crons/jobs/cleanup-orphaned-files.cron.job';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
@Command({
name: 'cron:file:cleanup-orphaned-files',
description: 'Starts a cron job to clean up orphaned files (no messageId)',
})
export class CleanupOrphanedFilesCronCommand extends CommandRunner {
constructor(
@InjectMessageQueue(MessageQueue.cronQueue)
private readonly messageQueueService: MessageQueueService,
) {
super();
}
async run(): Promise<void> {
await this.messageQueueService.addCron<undefined>({
jobName: CleanupOrphanedFilesCronJob.name,
data: undefined,
options: {
repeat: { pattern: CLEANUP_ORPHANED_FILES_CRON_PATTERN },
},
});
}
}
@@ -1,67 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { IsNull, LessThan, Repository } from 'typeorm';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileMetadataService } from 'src/engine/core-modules/file/services/file-metadata.service';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
export const CLEANUP_ORPHANED_FILES_CRON_PATTERN = '0 2 * * *';
@Processor(MessageQueue.cronQueue)
export class CleanupOrphanedFilesCronJob {
constructor(
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(FileEntity)
private readonly fileRepository: Repository<FileEntity>,
private readonly fileMetadataService: FileMetadataService,
) {}
@Process(CleanupOrphanedFilesCronJob.name)
@SentryCronMonitor(
CleanupOrphanedFilesCronJob.name,
CLEANUP_ORPHANED_FILES_CRON_PATTERN,
)
async handle(): Promise<void> {
const activeWorkspaces = await this.workspaceRepository.find({
where: {
activationStatus: WorkspaceActivationStatus.ACTIVE,
},
select: ['id'],
});
if (activeWorkspaces.length === 0) {
return;
}
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
const orphanedFiles = await this.fileRepository.find({
select: ['id', 'workspaceId', 'fullPath'],
where: {
messageId: IsNull(),
createdAt: LessThan(oneHourAgo),
},
});
if (orphanedFiles.length === 0) {
return;
}
for (const file of orphanedFiles) {
await this.fileMetadataService
.deleteFileById(file.id, file.workspaceId)
.catch((error) => {
throw new Error(
`[${CleanupOrphanedFilesCronJob.name}] Cannot delete orphaned file - ${file.fullPath}: ${error.message}`,
);
});
}
}
}
@@ -19,9 +19,6 @@ export class FileDTO {
@Field()
type: string;
@Field(() => UUIDScalarType, { nullable: true })
messageId?: string;
@Field(() => Date, { nullable: false })
createdAt: Date;
}
@@ -10,7 +10,6 @@ import {
} from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentChatMessageEntity } from 'src/engine/metadata-modules/agent/agent-chat-message.entity';
@Entity('file')
@Index('IDX_FILE_WORKSPACE_ID', ['workspaceId'])
@@ -39,15 +38,6 @@ export class FileEntity {
@JoinColumn({ name: 'workspaceId' })
workspace: Relation<Workspace>;
@Column({ nullable: true, type: 'uuid' })
messageId: string;
@ManyToOne(() => AgentChatMessageEntity, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'messageId' })
message: Relation<AgentChatMessageEntity>;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
}
@@ -11,8 +11,6 @@ import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FileController } from './controllers/file.controller';
import { CleanupOrphanedFilesCronCommand } from './crons/commands/cleanup-orphaned-files.cron.command';
import { CleanupOrphanedFilesCronJob } from './crons/jobs/cleanup-orphaned-files.cron.job';
import { FileEntity } from './entities/file.entity';
import { FileUploadService } from './file-upload/services/file-upload.service';
import { FileResolver } from './resolvers/file.resolver';
@@ -34,11 +32,9 @@ import { FileService } from './services/file.service';
FileWorkspaceMemberListener,
FileWorkspaceFolderDeletionJob,
FileDeletionJob,
CleanupOrphanedFilesCronJob,
CleanupOrphanedFilesCronCommand,
FileUploadService,
],
exports: [FileService, FileMetadataService, CleanupOrphanedFilesCronCommand],
exports: [FileService, FileMetadataService],
controllers: [FileController],
})
export class FileModule {}