diff --git a/service/task_executor/task.py b/service/task_executor/task.py
--- a/service/task_executor/task.py
+++ b/service/task_executor/task.py
@@ -396,8 +396,22 @@ def _check_executor_health(parse_doc_stats, running_snapshot):
     if running_count <= 0 and queue_size <= 0:
         return
 
     last_progress_ts = _executor_last_progress["ts"]
-    stalled = queue_size > 0 and (now - last_progress_ts > EXECUTOR_STALL_TIMEOUT_SEC)
+    seconds_since_progress = now - last_progress_ts
+    try:
+        max_workers = max(1, int(getattr(parse_doc_executor, "max_workers", PARSE_DOC_MAX_WORKERS)))
+    except Exception:
+        max_workers = PARSE_DOC_MAX_WORKERS
+
+    # Do not treat a full executor as stalled only because no whole document has
+    # finished recently. Large DOCX/PDF jobs can spend a long time inside image
+    # description, chunking, and vector upload while still making useful progress.
+    has_idle_capacity = running_count < max_workers
+    stalled = (
+        queue_size > 0
+        and has_idle_capacity
+        and seconds_since_progress > EXECUTOR_STALL_TIMEOUT_SEC
+    )
 
     oldest_age = 0.0
     for item in running_snapshot or []:
@@ -410,7 +424,10 @@ def _check_executor_health(parse_doc_stats, running_snapshot):
     if stalled or overdue:
         reason = []
         if stalled:
-            reason.append(f"无进度超过{EXECUTOR_STALL_TIMEOUT_SEC}s且队列仍有积压")
+            reason.append(
+                f"无完成进度超过{EXECUTOR_STALL_TIMEOUT_SEC}s且队列仍有积压，"
+                f"执行器有空闲槽位 running={running_count}/{max_workers}, queue={queue_size}"
+            )
         if overdue:
             reason.append(f"最老任务运行超过{EXECUTOR_MAX_TASK_AGE_SEC}s")
         _restart_parse_doc_executor("；".join(reason))
