where('archive_uid', trim($archiveUid)); } return (int) $query->count(); } public function countChunksByStatuses(array $statuses, ?string $archiveUid = null): int { $query = Db::table('chunks')->whereIn('embedding_status', $statuses); if ($archiveUid !== null && trim($archiveUid) !== '') { $query->where('archive_uid', trim($archiveUid)); } return (int) $query->count(); } public function resetAllChunksToPending(?string $archiveUid = null): int { $query = Db::table('chunks'); if ($archiveUid !== null && trim($archiveUid) !== '') { $query->where('archive_uid', trim($archiveUid)); } return $query->update($this->pendingResetPayload()); } public function resetRecoverableChunksToPending(?string $archiveUid = null): int { $query = Db::table('chunks') ->whereIn('embedding_status', [ EmbeddingStatus::QUEUED, EmbeddingStatus::PROCESSING, EmbeddingStatus::FAILED_RETRYABLE, ]); if ($archiveUid !== null && trim($archiveUid) !== '') { $query->where('archive_uid', trim($archiveUid)); } return $query->update($this->pendingResetPayload()); } public function queuePendingArchiveTasks(int $limit): array { $staleBefore = date('Y-m-d H:i:s', time() - max(60, (int) config('queue.tasks.stale_after_seconds', 900))); $archiveUids = Db::table('chunks') ->where(function ($query) use ($staleBefore): void { $query ->whereIn('embedding_status', [EmbeddingStatus::PENDING, EmbeddingStatus::FAILED_RETRYABLE]) ->orWhere(function ($stale) use ($staleBefore): void { $stale ->whereIn('embedding_status', [EmbeddingStatus::QUEUED, EmbeddingStatus::PROCESSING]) ->where(function ($time) use ($staleBefore): void { $time->whereNull('embedding_updated_at')->orWhere('embedding_updated_at', '<', $staleBefore); }); }); }) ->select('archive_uid') ->groupBy('archive_uid') ->orderByRaw('MIN(id)') ->limit($limit) ->pluck('archive_uid') ->all(); $archiveUids = array_values(array_filter(array_map('strval', $archiveUids))); foreach ($archiveUids as $archiveUid) { Db::table('chunks') ->where('archive_uid', $archiveUid) ->where(function ($query) use ($staleBefore): void { $query ->whereIn('embedding_status', [EmbeddingStatus::PENDING, EmbeddingStatus::FAILED_RETRYABLE]) ->orWhere(function ($stale) use ($staleBefore): void { $stale ->whereIn('embedding_status', [EmbeddingStatus::QUEUED, EmbeddingStatus::PROCESSING]) ->where(function ($time) use ($staleBefore): void { $time->whereNull('embedding_updated_at')->orWhere('embedding_updated_at', '<', $staleBefore); }); }); }) ->update([ 'embedding_status' => EmbeddingStatus::QUEUED, 'embedding_error' => null, 'embedding_updated_at' => Db::raw('CURRENT_TIMESTAMP'), ]); } return $archiveUids; } public function findQueuedChunks(string $archiveUid, int $limit): array { $chunks = Db::table('chunks') ->where('archive_uid', $archiveUid) ->whereIn('embedding_status', [EmbeddingStatus::QUEUED, EmbeddingStatus::PROCESSING]) ->orderBy('chunk_index') ->limit($limit) ->get(['chunk_uid', 'archive_uid', 'chunk_index', 'text']) ->all(); return array_map(static fn (object $chunk): array => [ 'chunk_uid' => (string) $chunk->chunk_uid, 'archive_uid' => (string) $chunk->archive_uid, 'chunk_index' => (int) $chunk->chunk_index, 'text' => (string) $chunk->text, ], $chunks); } public function markProcessing(array $chunkUids): void { $this->updateStatus($chunkUids, EmbeddingStatus::PROCESSING); } public function markEmbedded(string $chunkUid, array $embeddingRef, string $model): void { Db::table('chunks')->where('chunk_uid', $chunkUid)->update([ 'embedding_status' => EmbeddingStatus::EMBEDDED, 'embedding_ref' => json_encode($embeddingRef, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'embedding_model' => $model, 'embedding_error' => null, 'embedding_updated_at' => Db::raw('CURRENT_TIMESTAMP'), 'search_index_status' => 0, 'search_index_error' => null, 'search_index_updated_at' => null, ]); } public function markFailed(array $chunkUids, string $error, bool $retryable): void { if ($chunkUids === []) { return; } Db::table('chunks')->whereIn('chunk_uid', $chunkUids)->update([ 'embedding_status' => $retryable ? EmbeddingStatus::FAILED_RETRYABLE : EmbeddingStatus::FAILED_TERMINAL, 'embedding_error' => mb_substr($error, 0, 4000), 'embedding_updated_at' => Db::raw('CURRENT_TIMESTAMP'), ]); } private function updateStatus(array $chunkUids, int $status): void { if ($chunkUids === []) { return; } Db::table('chunks')->whereIn('chunk_uid', $chunkUids)->update([ 'embedding_status' => $status, 'embedding_updated_at' => Db::raw('CURRENT_TIMESTAMP'), ]); } private function pendingResetPayload(): array { return [ 'embedding_status' => EmbeddingStatus::PENDING, 'embedding_ref' => null, 'embedding_model' => null, 'embedding_error' => null, 'embedding_updated_at' => null, 'search_index_status' => 0, 'search_index_error' => null, 'search_index_updated_at' => null, ]; } }