R3.2.8.6-R1.2 Option A Local Resume Capability Reconciliation

#29
Files changed (2) hide show
  1. app.py +89 -7
  2. i18n.py +4 -0
app.py CHANGED
@@ -305,6 +305,76 @@ def _task_dir(task_id: str) -> Path:
305
  return Path(_user_workspace) / "tasks" / _safe_key(task_id)
306
 
307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  def _task_preview_path(task_id: str) -> str:
309
  """Return the unguessable local static preview URL for this task, if any."""
310
  request_path = _task_dir(task_id) / "request.json"
@@ -532,12 +602,15 @@ def _process_pending_resume() -> None:
532
  task_id = str(st.session_state.get("sp_pending_resume_task_id") or "").strip()
533
  if not task_id:
534
  return
535
- if not (_task_dir(task_id) / "request.json").is_file():
 
 
 
536
  st.session_state["sp_pending_resume_task_id"] = ""
537
- st.session_state["sp_pending_cancel_task_id"] = task_id
538
  st.session_state["sp_terminal_notice"] = {
539
- "kind": "error",
540
- "message": "Local checkpoint/request files are unavailable after runtime replacement; the reserved quota will be released.",
541
  }
542
  st.rerun()
543
  _launch_executor(task_id, force=True)
@@ -816,7 +889,8 @@ def _render_task_status_once(lang_code: str) -> None:
816
  _render_checkpoint_preview(task, lang_code)
817
 
818
  if status == "resumable":
819
- st.warning(tr(lang_code, "checkpoint_warning"))
 
820
  elif status == "completed":
821
  output_path = _task_output_path(task)
822
  if output_path and output_path.is_file():
@@ -914,9 +988,17 @@ def render_workspace_controls(user):
914
 
915
 
916
  if task_status == "resumable":
917
- st.warning(tr(lang_code, "checkpoint_warning"))
 
 
 
 
 
 
 
 
918
  colA, colB = st.columns([1, 1])
919
- if colA.button(tr(lang_code, "resume"), key="resume_task"):
920
  st.session_state["sp_pending_resume_task_id"] = str(task.get("task_id") or "")
921
  st.rerun()
922
  if colB.button(tr(lang_code, "clear"), key="clear_task"):
 
305
  return Path(_user_workspace) / "tasks" / _safe_key(task_id)
306
 
307
 
308
+ def _local_resume_capability(task: dict) -> tuple[bool, str]:
309
+ """Return whether this HF runtime has the local material required to resume.
310
+
311
+ Worker/D1 remains authoritative for the remote task state. This function
312
+ answers only the local capability question and never changes task/quota
313
+ state.
314
+ """
315
+ task = dict(task or {})
316
+ task_id = str(task.get("task_id") or "").strip()
317
+ if not task_id:
318
+ return False, "task_id_missing"
319
+
320
+ task_dir = _task_dir(task_id)
321
+ request_path = task_dir / "request.json"
322
+ if not request_path.is_file():
323
+ return False, "request_missing"
324
+ try:
325
+ request_payload = json.loads(request_path.read_text(encoding="utf-8"))
326
+ except Exception:
327
+ return False, "request_unreadable"
328
+ if not isinstance(request_payload, dict):
329
+ return False, "request_invalid"
330
+ if str(request_payload.get("task_id") or "").strip() != task_id:
331
+ return False, "request_task_mismatch"
332
+
333
+ request_hash = str(request_payload.get("request_hash") or "").strip().lower()
334
+ if not re.fullmatch(r"[a-f0-9]{64}", request_hash):
335
+ return False, "request_hash_invalid"
336
+ if request_hash != _canonical_request_hash(request_payload):
337
+ return False, "request_hash_mismatch"
338
+
339
+ task_root = task_dir.resolve()
340
+ for field in ("reference_files", "data_files", "interview_files"):
341
+ items = request_payload.get(field) or []
342
+ if not isinstance(items, list):
343
+ return False, f"{field}_invalid"
344
+ for item in items:
345
+ local_path = (task_dir / str(item)).resolve()
346
+ if task_root not in local_path.parents or not local_path.is_file():
347
+ return False, "input_missing"
348
+
349
+ try:
350
+ checkpoint_seq = int(task.get("checkpoint_seq") or 0)
351
+ except (TypeError, ValueError):
352
+ return False, "checkpoint_seq_invalid"
353
+ if checkpoint_seq <= 0:
354
+ return True, "ready"
355
+
356
+ expected_stage = str(task.get("checkpoint_stage") or "").strip()
357
+ try:
358
+ candidates = sorted((task_dir / "checkpoints").glob(f"checkpoint_{checkpoint_seq:05d}_*.json"))
359
+ except Exception:
360
+ candidates = []
361
+ for candidate in candidates:
362
+ try:
363
+ payload = json.loads(candidate.read_text(encoding="utf-8"))
364
+ if not isinstance(payload, dict):
365
+ continue
366
+ if str(payload.get("request_hash") or "").strip().lower() != request_hash:
367
+ continue
368
+ if int(payload.get("checkpoint_seq") or 0) != checkpoint_seq:
369
+ continue
370
+ if expected_stage and str(payload.get("checkpoint_stage") or "").strip() != expected_stage:
371
+ continue
372
+ return True, "ready"
373
+ except Exception:
374
+ continue
375
+ return False, "checkpoint_missing"
376
+
377
+
378
  def _task_preview_path(task_id: str) -> str:
379
  """Return the unguessable local static preview URL for this task, if any."""
380
  request_path = _task_dir(task_id) / "request.json"
 
602
  task_id = str(st.session_state.get("sp_pending_resume_task_id") or "").strip()
603
  if not task_id:
604
  return
605
+ task = dict(st.session_state.get("sp_generation_task") or {})
606
+ capable, reason = _local_resume_capability(task)
607
+ if str(task.get("task_id") or "").strip() != task_id or not capable:
608
+ logging.warning("[SP_UI] resume_unavailable task_id=%s reason=%s", task_id, reason)
609
  st.session_state["sp_pending_resume_task_id"] = ""
610
+ lang_code = str(st.session_state.get("sp_lang_code") or "zh")
611
  st.session_state["sp_terminal_notice"] = {
612
+ "kind": "warning",
613
+ "message": tr(lang_code, "resume_unavailable_runtime_replaced"),
614
  }
615
  st.rerun()
616
  _launch_executor(task_id, force=True)
 
889
  _render_checkpoint_preview(task, lang_code)
890
 
891
  if status == "resumable":
892
+ resume_capable, _ = _local_resume_capability(task)
893
+ st.warning(tr(lang_code, "checkpoint_warning" if resume_capable else "resume_unavailable_runtime_replaced"))
894
  elif status == "completed":
895
  output_path = _task_output_path(task)
896
  if output_path and output_path.is_file():
 
988
 
989
 
990
  if task_status == "resumable":
991
+ resume_capable, resume_reason = _local_resume_capability(task)
992
+ if resume_capable:
993
+ st.warning(tr(lang_code, "checkpoint_warning"))
994
+ else:
995
+ logging.warning(
996
+ "[SP_UI] local_resume_capability_missing task_id=%s reason=%s",
997
+ str(task.get("task_id") or ""), resume_reason,
998
+ )
999
+ st.warning(tr(lang_code, "resume_unavailable_runtime_replaced"))
1000
  colA, colB = st.columns([1, 1])
1001
+ if colA.button(tr(lang_code, "resume"), key="resume_task", disabled=not resume_capable):
1002
  st.session_state["sp_pending_resume_task_id"] = str(task.get("task_id") or "")
1003
  st.rerun()
1004
  if colB.button(tr(lang_code, "clear"), key="clear_task"):
i18n.py CHANGED
@@ -215,6 +215,7 @@ TEXTS: dict[str, dict[str, str]] = {
215
  "system_busy_warning": "🚨 有用户正在生成论文",
216
  "system_busy_info": "💡 预计25–35分钟后可正常使用",
217
  "checkpoint_warning": "⚠️ 检测到暂停的未完成论文任务。您可以继续原任务,或删除后重新开始。",
 
218
  "resume": "🔄 继续原任务",
219
  "clear": "🗑️ 删除并重新开始",
220
  "cancel_generation": "⛔ 取消生成",
@@ -385,6 +386,7 @@ TEXTS: dict[str, dict[str, str]] = {
385
  "system_busy_warning": "🚨 Another user is generating a paper",
386
  "system_busy_info": "💡 The system is expected to be available in 25–35 minutes",
387
  "checkpoint_warning": "⚠️ A paused unfinished paper task was detected. You can continue it or delete it and start again.",
 
388
  "resume": "🔄 Continue Previous Task",
389
  "clear": "🗑️ Delete and Start Again",
390
  "cancel_generation": "⛔ Cancel Generation",
@@ -555,6 +557,7 @@ TEXTS: dict[str, dict[str, str]] = {
555
  "system_busy_warning": "🚨 Другой пользователь формирует статью",
556
  "system_busy_info": "💡 Ожидаемое время доступности — через 25–35 минут",
557
  "checkpoint_warning": "⚠️ Обнаружена приостановленная незавершённая задача. Вы можете продолжить её или удалить и начать заново.",
 
558
  "resume": "🔄 Продолжить предыдущую задачу",
559
  "clear": "🗑️ Удалить и начать заново",
560
  "cancel_generation": "⛔ Отменить генерацию",
@@ -725,6 +728,7 @@ TEXTS: dict[str, dict[str, str]] = {
725
  "system_busy_warning": "🚨 Басқа пайдаланушы мақала дайындап жатыр",
726
  "system_busy_info": "💡 Жүйе шамамен 25–35 минуттан кейін қолжетімді болады",
727
  "checkpoint_warning": "⚠️ Аяқталмаған, уақытша тоқтатылған тапсырма анықталды. Оны жалғастыруға немесе өшіріп, қайта бастауға болады.",
 
728
  "resume": "🔄 Алдыңғы тапсырманы жалғастыру",
729
  "clear": "🗑️ Өшіріп, қайта бастау",
730
  "cancel_generation": "⛔ Генерацияны тоқтату",
 
215
  "system_busy_warning": "🚨 有用户正在生成论文",
216
  "system_busy_info": "💡 预计25–35分钟后可正常使用",
217
  "checkpoint_warning": "⚠️ 检测到暂停的未完成论文任务。您可以继续原任务,或删除后重新开始。",
218
+ "resume_unavailable_runtime_replaced": "⚠️ 检测到暂停的未完成论文任务,但当前运行环境缺少继续所需的本地请求或断点文件。为避免错误恢复,继续按钮已禁用;如需释放本任务并重新开始,请明确点击“删除并重新开始”。",
219
  "resume": "🔄 继续原任务",
220
  "clear": "🗑️ 删除并重新开始",
221
  "cancel_generation": "⛔ 取消生成",
 
386
  "system_busy_warning": "🚨 Another user is generating a paper",
387
  "system_busy_info": "💡 The system is expected to be available in 25–35 minutes",
388
  "checkpoint_warning": "⚠️ A paused unfinished paper task was detected. You can continue it or delete it and start again.",
389
+ "resume_unavailable_runtime_replaced": "⚠️ A paused unfinished paper task was detected, but this runtime no longer has the local request or checkpoint files required to continue it. Resume has been disabled to prevent an unsafe recovery. To release this task and start again, explicitly choose ‘Delete and Start Again’.",
390
  "resume": "🔄 Continue Previous Task",
391
  "clear": "🗑️ Delete and Start Again",
392
  "cancel_generation": "⛔ Cancel Generation",
 
557
  "system_busy_warning": "🚨 Другой пользователь формирует статью",
558
  "system_busy_info": "💡 Ожидаемое время доступности — через 25–35 минут",
559
  "checkpoint_warning": "⚠️ Обнаружена приостановленная незавершённая задача. Вы можете продолжить её или удалить и начать заново.",
560
+ "resume_unavailable_runtime_replaced": "⚠️ Обнаружена приостановленная незавершённая задача, но в текущей среде выполнения отсутствуют локальный файл запроса или контрольная точка, необходимые для продолжения. Чтобы избежать некорректного восстановления, кнопка продолжения отключена. Для освобождения задачи и нового запуска явно выберите «Удалить и начать заново».",
561
  "resume": "🔄 Продолжить предыдущую задачу",
562
  "clear": "🗑️ Удалить и начать заново",
563
  "cancel_generation": "⛔ Отменить генерацию",
 
728
  "system_busy_warning": "🚨 Басқа пайдаланушы мақала дайындап жатыр",
729
  "system_busy_info": "💡 Жүйе шамамен 25–35 минуттан кейін қолжетімді болады",
730
  "checkpoint_warning": "⚠️ Аяқталмаған, уақытша тоқтатылған тапсырма анықталды. Оны жалғастыруға немесе өшіріп, қайта бастауға болады.",
731
+ "resume_unavailable_runtime_replaced": "⚠️ Уақытша тоқтатылған аяқталмаған тапсырма анықталды, бірақ ағымдағы іске қосу ортасында оны жалғастыруға қажет жергілікті сұрау немесе бақылау нүктесі файлдары жоқ. Қате қалпына келтіруге жол бермеу үшін жалғастыру түймесі өшірілді. Тапсырманы босатып, қайта бастау үшін «Өшіріп, қайта бастау» әрекетін нақты таңдаңыз.",
732
  "resume": "🔄 Алдыңғы тапсырманы жалғастыру",
733
  "clear": "🗑️ Өшіріп, қайта бастау",
734
  "cancel_generation": "⛔ Генерацияны тоқтату",