Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,11 @@ public function __invoke(RunEditSessionMessage $message): void
$session->setStatus(EditSessionStatus::Running);
$this->entityManager->flush();

$conversation = $session->getConversation();

try {
// Load previous messages from conversation
$previousMessages = $this->loadPreviousMessages($session);
$conversation = $session->getConversation();

// Set execution context for agent container execution
$workspace = $this->workspaceMgmtFacade->getWorkspaceById($conversation->getWorkspaceId());
Expand Down Expand Up @@ -121,29 +122,8 @@ public function __invoke(RunEditSessionMessage $message): void
$streamEndedWithFailure = false;

foreach ($generator as $chunk) {
// Cooperative cancellation: check status directly via DBAL to avoid
// entityManager->refresh() which fails on readonly entity properties.
$currentStatus = $this->entityManager->getConnection()->fetchOne(
'SELECT status FROM edit_sessions WHERE id = ?',
[$session->getId()]
);

if ($currentStatus === EditSessionStatus::Cancelling->value) {
// Persist a synthetic assistant message so the LLM on the next turn
// understands this turn was interrupted and won't try to answer it.
new ConversationMessage(
$conversation,
ConversationMessageRole::Assistant,
json_encode(
['content' => '[Cancelled by the user — disregard this turn.]'],
JSON_THROW_ON_ERROR
)
);

EditSessionChunk::createDoneChunk($session, false, 'Cancelled by user.');
$session->setStatus(EditSessionStatus::Cancelled);
$this->entityManager->flush();

if ($this->isCancellationRequested($session)) {
$this->finalizeCancelledSession($session, $conversation);
return;
}

Expand All @@ -159,6 +139,11 @@ public function __invoke(RunEditSessionMessage $message): void
// Persist new conversation messages
$this->persistConversationMessage($conversation, $chunk->message);
} elseif ($chunk->chunkType === EditStreamChunkType::Done) {
if (($chunk->success ?? false) !== true && $this->isCancellationRequested($session)) {
$this->finalizeCancelledSession($session, $conversation);
return;
}

$streamEndedWithFailure = ($chunk->success ?? false) !== true;
EditSessionChunk::createDoneChunk(
$session,
Expand All @@ -171,6 +156,12 @@ public function __invoke(RunEditSessionMessage $message): void
}

if ($streamEndedWithFailure) {
if ($this->isCancellationRequested($session)) {
$session->setStatus(EditSessionStatus::Cancelled);
$this->entityManager->flush();
return;
}

$session->setStatus(EditSessionStatus::Failed);
$this->entityManager->flush();

Expand All @@ -183,6 +174,11 @@ public function __invoke(RunEditSessionMessage $message): void
// Commit and push changes after successful edit session
$this->commitChangesAfterEdit($conversation, $session);
} catch (Throwable $e) {
if ($this->isCancellationRequested($session)) {
$this->finalizeCancelledSession($session, $conversation);
return;
}

$this->logger->error('EditSession failed', [
'sessionId' => $message->sessionId,
'error' => $e->getMessage(),
Expand Down Expand Up @@ -309,4 +305,41 @@ private function truncateMessage(string $message, int $maxLength): string

return mb_substr($message, 0, $maxLength - 3) . '...';
}

private function isCancellationRequested(EditSession $session): bool
{
if ($session->getStatus() === EditSessionStatus::Cancelling) {
return true;
}

$sessionId = $session->getId();
if ($sessionId === null) {
return false;
}

// Query status directly to detect cancellation from parallel requests immediately.
$currentStatus = $this->entityManager->getConnection()->fetchOne(
'SELECT status FROM edit_sessions WHERE id = ?',
[$sessionId]
);

return $currentStatus === EditSessionStatus::Cancelling->value;
}

private function finalizeCancelledSession(EditSession $session, Conversation $conversation): void
{
// Keep conversation context consistent for future turns.
new ConversationMessage(
$conversation,
ConversationMessageRole::Assistant,
json_encode(
['content' => '[Cancelled by the user — disregard this turn.]'],
JSON_THROW_ON_ERROR
)
);

EditSessionChunk::createDoneChunk($session, false, 'Cancelled by user.');
$session->setStatus(EditSessionStatus::Cancelled);
$this->entityManager->flush();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use App\RemoteContentAssets\Facade\RemoteContentAssetsFacadeInterface;
use App\WorkspaceMgmt\Facade\Enum\WorkspaceStatus;
use App\WorkspaceMgmt\Facade\WorkspaceMgmtFacadeInterface;
use App\WorkspaceTooling\Facade\WorkspaceToolingServiceInterface;
use Doctrine\ORM\EntityManagerInterface;
use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
Expand Down Expand Up @@ -60,6 +61,7 @@ public function __construct(
private readonly TranslatorInterface $translator,
private readonly PromptSuggestionsService $promptSuggestionsService,
private readonly LlmContentEditorFacadeInterface $llmContentEditorFacade,
private readonly WorkspaceToolingServiceInterface $workspaceToolingFacade,
) {
}

Expand Down Expand Up @@ -656,6 +658,19 @@ public function cancel(
$session->setStatus(EditSessionStatus::Cancelling);
$this->entityManager->flush();

$conversationId = $conversation->getId();
if ($conversationId !== null) {
try {
// Best-effort hard stop for long-running tool/runtime containers.
$this->workspaceToolingFacade->stopAgentContainersForConversation(
$conversation->getWorkspaceId(),
$conversationId
);
} catch (Throwable) {
// If runtime interruption fails, cooperative cancellation still applies.
}
}

return $this->json(['success' => true]);
}

Expand Down
Loading
Loading