vibego 1.0.2__py3-none-any.whl → 1.0.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bot.py CHANGED
@@ -248,6 +248,7 @@ CODEX_SESSION_FILE_PATH = os.environ.get("CODEX_SESSION_FILE_PATH", "").strip()
248
248
  CODEX_SESSIONS_ROOT = os.environ.get("CODEX_SESSIONS_ROOT", "").strip()
249
249
  MODEL_SESSION_ROOT = os.environ.get("MODEL_SESSION_ROOT", "").strip()
250
250
  MODEL_SESSION_GLOB = os.environ.get("MODEL_SESSION_GLOB", "rollout-*.jsonl").strip() or "rollout-*.jsonl"
251
+ SESSION_LOCK_FILE_PATH = os.environ.get("SESSION_LOCK_FILE_PATH", "").strip()
251
252
  SESSION_POLL_TIMEOUT = float(os.environ.get("SESSION_POLL_TIMEOUT", "2"))
252
253
  WATCH_MAX_WAIT = float(os.environ.get("WATCH_MAX_WAIT", "0"))
253
254
  WATCH_INTERVAL = float(os.environ.get("WATCH_INTERVAL", "2"))
@@ -257,6 +258,7 @@ SEND_FAILURE_NOTICE_COOLDOWN = float(os.environ.get("SEND_FAILURE_NOTICE_COOLDOW
257
258
  SESSION_INITIAL_BACKTRACK_BYTES = int(os.environ.get("SESSION_INITIAL_BACKTRACK_BYTES", "16384"))
258
259
  ENABLE_PLAN_PROGRESS = (os.environ.get("ENABLE_PLAN_PROGRESS", "1").strip().lower() not in {"0", "false", "no", "off"})
259
260
  AUTO_COMPACT_THRESHOLD = max(_env_int("AUTO_COMPACT_THRESHOLD", 0), 0)
261
+ SESSION_LOCK_REQUIRED = (os.environ.get("SESSION_LOCK_REQUIRED", "1").strip().lower() not in {"0", "false", "no", "off"})
260
262
 
261
263
  PLAN_STATUS_LABELS = {
262
264
  "completed": "✅",
@@ -956,7 +958,7 @@ async def _send_session_ack(
956
958
  model_label = (ACTIVE_MODEL or "Model").strip() or "Model"
957
959
  session_id = session_path.stem if session_path else "unknown"
958
960
  prompt_message = (
959
- f"💭 {model_label} is processing. Listening for model output...\n"
961
+ f"💭 {model_label} is Thinking... Listening for model output...\n"
960
962
  f"sessionId : {session_id}"
961
963
  )
962
964
  ack_message = await _reply_to_chat(
@@ -1036,52 +1038,84 @@ async def _dispatch_prompt_to_model(
1036
1038
  if CODEX_SESSION_FILE_PATH:
1037
1039
  pointer_path = resolve_path(CODEX_SESSION_FILE_PATH)
1038
1040
 
1039
- if pointer_path is not None and session_path is None:
1040
- session_path = _read_pointer_path(pointer_path)
1041
+ lock_session = _read_session_lock_path()
1042
+ lock_required = _is_session_lock_enforced()
1043
+ if lock_session is not None:
1044
+ if session_path is None:
1045
+ worker_log.info(
1046
+ "[session-map] chat=%s use lock session %s",
1047
+ chat_id,
1048
+ lock_session,
1049
+ extra=_session_extra(path=lock_session),
1050
+ )
1051
+ elif session_path != lock_session:
1052
+ worker_log.info(
1053
+ "[session-map] chat=%s override session with lock %s",
1054
+ chat_id,
1055
+ lock_session,
1056
+ extra=_session_extra(path=lock_session),
1057
+ )
1058
+ _sync_pointer_with_lock(pointer_path, lock_session)
1059
+ session_path = lock_session
1060
+ else:
1041
1061
  if session_path is not None:
1042
1062
  worker_log.info(
1043
- "[session-map] chat=%s pointer -> %s",
1063
+ "[session-map] chat=%s reuse session %s",
1044
1064
  chat_id,
1045
1065
  session_path,
1046
1066
  extra=_session_extra(path=session_path),
1047
1067
  )
1048
- elif session_path is not None:
1049
- worker_log.info(
1050
- "[session-map] chat=%s reuse session %s",
1051
- chat_id,
1052
- session_path,
1053
- extra=_session_extra(path=session_path),
1054
- )
1068
+ elif lock_required:
1069
+ await _reply_to_chat(
1070
+ chat_id,
1071
+ _session_lock_missing_message(),
1072
+ reply_to=reply_to,
1073
+ )
1074
+ worker_log.error(
1075
+ "[session-lock] Session lock required but missing",
1076
+ extra={"chat": chat_id, "lock_file": SESSION_LOCK_FILE_PATH or "-"},
1077
+ )
1078
+ return False, None
1079
+ else:
1080
+ if pointer_path is not None and session_path is None:
1081
+ session_path = _read_pointer_path(pointer_path)
1082
+ if session_path is not None:
1083
+ worker_log.info(
1084
+ "[session-map] chat=%s pointer -> %s",
1085
+ chat_id,
1086
+ session_path,
1087
+ extra=_session_extra(path=session_path),
1088
+ )
1055
1089
 
1056
- target_cwd = CODEX_WORKDIR if CODEX_WORKDIR else None
1057
- if pointer_path is not None:
1058
- current_cwd = _read_session_meta_cwd(session_path) if session_path else None
1059
- if session_path is None or (target_cwd and current_cwd != target_cwd):
1060
- latest = _find_latest_rollout_for_cwd(pointer_path, target_cwd)
1061
- if latest is not None:
1062
- try:
1063
- SESSION_OFFSETS[str(latest)] = latest.stat().st_size
1064
- except FileNotFoundError:
1065
- SESSION_OFFSETS[str(latest)] = 0
1066
- _update_pointer(pointer_path, latest)
1067
- session_path = latest
1068
- worker_log.info(
1069
- "[session-map] chat=%s switch to cwd-matched %s",
1070
- chat_id,
1071
- session_path,
1072
- extra=_session_extra(path=session_path),
1073
- )
1074
- if _is_claudecode_model():
1075
- fallback = _find_latest_claudecode_rollout(pointer_path)
1076
- if fallback is not None and fallback != session_path:
1077
- _update_pointer(pointer_path, fallback)
1078
- session_path = fallback
1079
- worker_log.info(
1080
- "[session-map] chat=%s fallback to ClaudeCode session %s",
1081
- chat_id,
1082
- session_path,
1083
- extra=_session_extra(path=session_path),
1084
- )
1090
+ target_cwd = CODEX_WORKDIR if CODEX_WORKDIR else None
1091
+ if pointer_path is not None:
1092
+ current_cwd = _read_session_meta_cwd(session_path) if session_path else None
1093
+ if session_path is None or (target_cwd and current_cwd != target_cwd):
1094
+ latest = _find_latest_rollout_for_cwd(pointer_path, target_cwd)
1095
+ if latest is not None:
1096
+ try:
1097
+ SESSION_OFFSETS[str(latest)] = latest.stat().st_size
1098
+ except FileNotFoundError:
1099
+ SESSION_OFFSETS[str(latest)] = 0
1100
+ _update_pointer(pointer_path, latest)
1101
+ session_path = latest
1102
+ worker_log.info(
1103
+ "[session-map] chat=%s (lock disabled) switch to cwd-matched %s",
1104
+ chat_id,
1105
+ session_path,
1106
+ extra=_session_extra(path=session_path),
1107
+ )
1108
+ if _is_claudecode_model():
1109
+ fallback = _find_latest_claudecode_rollout(pointer_path)
1110
+ if fallback is not None and fallback != session_path:
1111
+ _update_pointer(pointer_path, fallback)
1112
+ session_path = fallback
1113
+ worker_log.info(
1114
+ "[session-map] chat=%s (lock disabled) fallback to ClaudeCode session %s",
1115
+ chat_id,
1116
+ session_path,
1117
+ extra=_session_extra(path=session_path),
1118
+ )
1085
1119
 
1086
1120
  needs_session_wait = session_path is None
1087
1121
  if needs_session_wait and pointer_path is None:
@@ -1820,8 +1854,33 @@ BOT_COMMANDS: list[tuple[str, str]] = [
1820
1854
  COMMAND_KEYWORDS: set[str] = {command for command, _ in BOT_COMMANDS}
1821
1855
  COMMAND_KEYWORDS.update({"task_child", "task_children", "task_delete"})
1822
1856
 
1823
- WORKER_MENU_BUTTON_TEXT = "📋 Task List"
1824
- WORKER_CREATE_TASK_BUTTON_TEXT = "➕ Create Task"
1857
+ def _button_text_variants(env_key: str, fallback: Sequence[str]) -> tuple[str, ...]:
1858
+ """Return button label candidates sourced from env or fallback list."""
1859
+
1860
+ raw = (os.environ.get(env_key) or "").strip()
1861
+ if not raw:
1862
+ return tuple(fallback)
1863
+ variants = []
1864
+ for segment in raw.split("|"):
1865
+ cleaned = segment.strip()
1866
+ if cleaned and cleaned not in variants:
1867
+ variants.append(cleaned)
1868
+ return tuple(variants or fallback)
1869
+
1870
+
1871
+ WORKER_MENU_BUTTON_TEXT_VARIANTS = _button_text_variants(
1872
+ "WORKER_MENU_BUTTON_TEXTS",
1873
+ ("📋 Task List", "📋 任务列表"),
1874
+ )
1875
+ WORKER_CREATE_TASK_BUTTON_TEXT_VARIANTS = _button_text_variants(
1876
+ "WORKER_CREATE_TASK_BUTTON_TEXTS",
1877
+ ("➕ Create Task", "➕ 创建任务"),
1878
+ )
1879
+
1880
+ WORKER_MENU_BUTTON_TEXT = WORKER_MENU_BUTTON_TEXT_VARIANTS[0]
1881
+ WORKER_CREATE_TASK_BUTTON_TEXT = WORKER_CREATE_TASK_BUTTON_TEXT_VARIANTS[0]
1882
+ WORKER_MENU_BUTTON_TEXT_SET = set(WORKER_MENU_BUTTON_TEXT_VARIANTS)
1883
+ WORKER_CREATE_TASK_BUTTON_TEXT_SET = set(WORKER_CREATE_TASK_BUTTON_TEXT_VARIANTS)
1825
1884
 
1826
1885
  TASK_ID_VALID_PATTERN = re.compile(r"^TASK_[A-Z0-9_]+$")
1827
1886
  TASK_ID_USAGE_TIP = "Invalid task ID format. Use patterns like TASK_0001."
@@ -3010,6 +3069,9 @@ async def _build_history_context_for_model(task_id: str) -> tuple[str, int]:
3010
3069
  return "\n".join(trimmed_lines), len(trimmed_lines)
3011
3070
 
3012
3071
 
3072
+ SKIPPED_TASK_HISTORY_ACTIONS: set[str] = {"push_model", "summary_request"}
3073
+
3074
+
3013
3075
  async def _log_task_action(
3014
3076
  task_id: str,
3015
3077
  *,
@@ -3023,11 +3085,20 @@ async def _log_task_action(
3023
3085
  ) -> None:
3024
3086
  """Encapsulate task event writing and record logs when exceptions occur to avoid interrupting the main process."""
3025
3087
 
3088
+ action_token = (action or "").strip()
3089
+ if action_token in SKIPPED_TASK_HISTORY_ACTIONS:
3090
+ worker_log.debug(
3091
+ "Skipped logging task action in history: task_id=%s action=%s",
3092
+ task_id,
3093
+ action_token,
3094
+ extra=_session_extra(),
3095
+ )
3096
+ return
3026
3097
  data_payload: Optional[Dict[str, Any]]
3027
3098
  if payload is None:
3028
- data_payload = {"action": action}
3099
+ data_payload = {"action": action_token}
3029
3100
  else:
3030
- data_payload = {"action": action, **payload}
3101
+ data_payload = {"action": action_token, **payload}
3031
3102
  try:
3032
3103
  await TASK_SERVICE.log_task_event(
3033
3104
  task_id,
@@ -4298,29 +4369,12 @@ async def _log_model_reply_event(
4298
4369
  session_path: Path,
4299
4370
  event_offset: int,
4300
4371
  ) -> None:
4301
- """Write model responses to Task history."""
4372
+ """Model replies are no longer persisted to history."""
4302
4373
 
4303
- trimmed = _trim_history_value(content, limit=HISTORY_DISPLAY_VALUE_LIMIT)
4304
- payload = {
4305
- "model": ACTIVE_MODEL or "",
4306
- "session": str(session_path),
4307
- "offset": event_offset,
4308
- }
4309
- if content:
4310
- payload["content"] = content[:MODEL_REPLY_PAYLOAD_LIMIT]
4311
- try:
4312
- await TASK_SERVICE.log_task_event(
4313
- task_id,
4314
- event_type=HISTORY_EVENT_MODEL_REPLY,
4315
- actor=f"model/{ACTIVE_MODEL or 'codex'}",
4316
- new_value=trimmed,
4317
- payload=payload,
4318
- )
4319
- except ValueError:
4320
- worker_log.warning(
4321
- "The model reply writes fail: Task does not exist",
4322
- extra={"task_id": task_id, **_session_extra(path=session_path)},
4323
- )
4374
+ worker_log.debug(
4375
+ "Skipping history write for model reply",
4376
+ extra={"task_id": task_id, "session": str(session_path)},
4377
+ )
4324
4378
 
4325
4379
 
4326
4380
  async def _maybe_finalize_summary(
@@ -4792,60 +4846,79 @@ async def _ensure_session_watcher(chat_id: int) -> Optional[Path]:
4792
4846
 
4793
4847
  target_cwd = CODEX_WORKDIR or None
4794
4848
 
4795
- if session_path is None and pointer_path is not None:
4796
- session_path = _read_pointer_path(pointer_path)
4797
- if session_path is not None:
4849
+ lock_session = _read_session_lock_path()
4850
+ lock_required = _is_session_lock_enforced()
4851
+ if lock_session is not None:
4852
+ if session_path is None or session_path != lock_session:
4798
4853
  worker_log.info(
4799
- "[session-map] chat=%s pointer -> %s",
4854
+ "[session-map] chat=%s use lock session %s",
4800
4855
  chat_id,
4801
- session_path,
4802
- extra=_session_extra(path=session_path),
4856
+ lock_session,
4857
+ extra=_session_extra(path=lock_session),
4803
4858
  )
4804
- if session_path is None and pointer_path is not None:
4805
- latest = _find_latest_rollout_for_cwd(pointer_path, target_cwd)
4806
- if latest is not None:
4807
- session_path = latest
4808
- _update_pointer(pointer_path, latest)
4809
- worker_log.info(
4810
- "[session-map] chat=%s locate latest rollout %s",
4811
- chat_id,
4812
- session_path,
4813
- extra=_session_extra(path=session_path),
4859
+ _sync_pointer_with_lock(pointer_path, lock_session)
4860
+ session_path = lock_session
4861
+ else:
4862
+ if lock_required:
4863
+ worker_log.error(
4864
+ "[session-lock] Session lock required but missing during watcher ensure",
4865
+ extra={"chat": chat_id, "lock_file": SESSION_LOCK_FILE_PATH or "-"},
4814
4866
  )
4867
+ return None
4868
+ if session_path is None and pointer_path is not None:
4869
+ session_path = _read_pointer_path(pointer_path)
4870
+ if session_path is not None:
4871
+ worker_log.info(
4872
+ "[session-map] chat=%s pointer -> %s",
4873
+ chat_id,
4874
+ session_path,
4875
+ extra=_session_extra(path=session_path),
4876
+ )
4877
+ if session_path is None and pointer_path is not None:
4878
+ latest = _find_latest_rollout_for_cwd(pointer_path, target_cwd)
4879
+ if latest is not None:
4880
+ session_path = latest
4881
+ _update_pointer(pointer_path, latest)
4882
+ worker_log.info(
4883
+ "[session-map] chat=%s locate latest rollout %s",
4884
+ chat_id,
4885
+ session_path,
4886
+ extra=_session_extra(path=session_path),
4887
+ )
4815
4888
 
4816
- if pointer_path is not None and _is_claudecode_model():
4817
- fallback = _find_latest_claudecode_rollout(pointer_path)
4818
- if fallback is not None and fallback != session_path:
4819
- session_path = fallback
4820
- _update_pointer(pointer_path, session_path)
4821
- worker_log.info(
4822
- "[session-map] chat=%s resume ClaudeCode session %s",
4823
- chat_id,
4824
- session_path,
4825
- extra=_session_extra(path=session_path),
4826
- )
4889
+ if pointer_path is not None and _is_claudecode_model():
4890
+ fallback = _find_latest_claudecode_rollout(pointer_path)
4891
+ if fallback is not None and fallback != session_path:
4892
+ session_path = fallback
4893
+ _update_pointer(pointer_path, session_path)
4894
+ worker_log.info(
4895
+ "[session-map] chat=%s resume ClaudeCode session %s",
4896
+ chat_id,
4897
+ session_path,
4898
+ extra=_session_extra(path=session_path),
4899
+ )
4827
4900
 
4828
- if session_path is None and pointer_path is not None:
4829
- session_path = await _await_session_path(pointer_path, target_cwd)
4830
- if session_path is not None:
4831
- _update_pointer(pointer_path, session_path)
4832
- worker_log.info(
4833
- "[session-map] chat=%s bind fresh session %s",
4834
- chat_id,
4835
- session_path,
4836
- extra=_session_extra(path=session_path),
4837
- )
4838
- if session_path is None and pointer_path is not None and _is_claudecode_model():
4839
- fallback = _find_latest_claudecode_rollout(pointer_path)
4840
- if fallback is not None:
4841
- session_path = fallback
4842
- _update_pointer(pointer_path, session_path)
4843
- worker_log.info(
4844
- "[session-map] chat=%s fallback bind ClaudeCode session %s",
4845
- chat_id,
4846
- session_path,
4847
- extra=_session_extra(path=session_path),
4848
- )
4901
+ if session_path is None and pointer_path is not None:
4902
+ session_path = await _await_session_path(pointer_path, target_cwd)
4903
+ if session_path is not None:
4904
+ _update_pointer(pointer_path, session_path)
4905
+ worker_log.info(
4906
+ "[session-map] chat=%s bind fresh session %s",
4907
+ chat_id,
4908
+ session_path,
4909
+ extra=_session_extra(path=session_path),
4910
+ )
4911
+ if session_path is None and pointer_path is not None and _is_claudecode_model():
4912
+ fallback = _find_latest_claudecode_rollout(pointer_path)
4913
+ if fallback is not None:
4914
+ session_path = fallback
4915
+ _update_pointer(pointer_path, session_path)
4916
+ worker_log.info(
4917
+ "[session-map] chat=%s fallback bind ClaudeCode session %s",
4918
+ chat_id,
4919
+ session_path,
4920
+ extra=_session_extra(path=session_path),
4921
+ )
4849
4922
 
4850
4923
  if session_path is None:
4851
4924
  worker_log.warning(
@@ -5268,6 +5341,115 @@ async def _watch_and_notify(chat_id: int, session_path: Path,
5268
5341
  )
5269
5342
 
5270
5343
 
5344
+ _SESSION_LOCK_CACHE_MTIME: Optional[float] = None
5345
+ _SESSION_LOCK_CACHE_VALUE: Optional[Path] = None
5346
+
5347
+
5348
+ def _session_lock_file() -> Optional[Path]:
5349
+ if not SESSION_LOCK_FILE_PATH:
5350
+ return None
5351
+ return resolve_path(SESSION_LOCK_FILE_PATH)
5352
+
5353
+
5354
+ def _is_session_lock_enforced() -> bool:
5355
+ """Return True when the worker must rely on the captured session lock."""
5356
+
5357
+ return SESSION_LOCK_REQUIRED and bool(SESSION_LOCK_FILE_PATH)
5358
+
5359
+
5360
+ def _session_lock_missing_message() -> str:
5361
+ """Build a human-readable error when the session lock is missing."""
5362
+
5363
+ target = SESSION_LOCK_FILE_PATH or "session_lock.json"
5364
+ return (
5365
+ "当前 worker 未检测到会话锁,无法定位本项目独占的模型会话。\n"
5366
+ f"缺失的锁文件:{target}\n"
5367
+ "请在对应项目目录重新执行 scripts/run_bot.sh(或等效启动脚本)以捕获新的 tmux 会话。"
5368
+ )
5369
+
5370
+
5371
+ def _read_session_lock_path() -> Optional[Path]:
5372
+ """Read the persisted session lock and return the rollout path when valid."""
5373
+
5374
+ lock_file = _session_lock_file()
5375
+ if lock_file is None:
5376
+ return None
5377
+ global _SESSION_LOCK_CACHE_MTIME, _SESSION_LOCK_CACHE_VALUE
5378
+
5379
+ try:
5380
+ stat = lock_file.stat()
5381
+ except FileNotFoundError:
5382
+ _SESSION_LOCK_CACHE_MTIME = None
5383
+ _SESSION_LOCK_CACHE_VALUE = None
5384
+ return None
5385
+
5386
+ mtime = stat.st_mtime
5387
+ if _SESSION_LOCK_CACHE_MTIME == mtime and _SESSION_LOCK_CACHE_VALUE is not None:
5388
+ return _SESSION_LOCK_CACHE_VALUE
5389
+
5390
+ try:
5391
+ raw = lock_file.read_text(encoding="utf-8")
5392
+ except OSError:
5393
+ _SESSION_LOCK_CACHE_MTIME = mtime
5394
+ _SESSION_LOCK_CACHE_VALUE = None
5395
+ return None
5396
+ try:
5397
+ payload = json.loads(raw)
5398
+ except json.JSONDecodeError:
5399
+ worker_log.warning(
5400
+ "[session-lock] Invalid JSON payload",
5401
+ extra={"lock": str(lock_file)},
5402
+ )
5403
+ _SESSION_LOCK_CACHE_MTIME = mtime
5404
+ _SESSION_LOCK_CACHE_VALUE = None
5405
+ return None
5406
+
5407
+ session_raw = payload.get("session_path")
5408
+ if not isinstance(session_raw, str) or not session_raw.strip():
5409
+ _SESSION_LOCK_CACHE_MTIME = mtime
5410
+ _SESSION_LOCK_CACHE_VALUE = None
5411
+ return None
5412
+
5413
+ rollout = resolve_path(session_raw.strip())
5414
+ if not rollout.exists():
5415
+ worker_log.warning(
5416
+ "[session-lock] Recorded session file is missing",
5417
+ extra={"session": str(rollout)},
5418
+ )
5419
+ _SESSION_LOCK_CACHE_MTIME = mtime
5420
+ _SESSION_LOCK_CACHE_VALUE = None
5421
+ return None
5422
+
5423
+ tmux_name = payload.get("tmux_session")
5424
+ if tmux_name and isinstance(tmux_name, str) and tmux_name.strip() and tmux_name.strip() != TMUX_SESSION:
5425
+ worker_log.info(
5426
+ "[session-lock] tmux mismatch, ignoring lock",
5427
+ extra={"lock_session": tmux_name.strip(), "tmux": TMUX_SESSION},
5428
+ )
5429
+ _SESSION_LOCK_CACHE_MTIME = mtime
5430
+ _SESSION_LOCK_CACHE_VALUE = None
5431
+ return None
5432
+
5433
+ _SESSION_LOCK_CACHE_MTIME = mtime
5434
+ _SESSION_LOCK_CACHE_VALUE = rollout
5435
+ return rollout
5436
+
5437
+
5438
+ def _sync_pointer_with_lock(pointer: Optional[Path], lock_path: Path) -> None:
5439
+ """Ensure pointer.txt matches the locked session path."""
5440
+
5441
+ if pointer is None:
5442
+ return
5443
+ target = str(lock_path)
5444
+ try:
5445
+ current = pointer.read_text(encoding="utf-8").strip()
5446
+ except OSError:
5447
+ current = ""
5448
+ if current == target:
5449
+ return
5450
+ _update_pointer(pointer, lock_path)
5451
+
5452
+
5271
5453
  def _read_pointer_path(pointer: Path) -> Optional[Path]:
5272
5454
  try:
5273
5455
  raw = pointer.read_text(encoding="utf-8").strip()
@@ -5483,7 +5665,7 @@ def _format_plan_update(arguments: Any, *, event_timestamp: Optional[str]) -> Op
5483
5665
  if not steps:
5484
5666
  return None
5485
5667
 
5486
- header = "currentTaskExecution plan:"
5668
+ header = "current task execution plan:"
5487
5669
  body_parts = [header]
5488
5670
  if lines:
5489
5671
  body_parts.extend(lines)
@@ -5945,7 +6127,7 @@ async def on_task_list(message: Message) -> None:
5945
6127
  await _handle_task_list_request(message)
5946
6128
 
5947
6129
 
5948
- @router.message(F.text == WORKER_MENU_BUTTON_TEXT)
6130
+ @router.message(F.text.in_(WORKER_MENU_BUTTON_TEXT_SET))
5949
6131
  async def on_task_list_button(message: Message) -> None:
5950
6132
  await _handle_task_list_request(message)
5951
6133
 
@@ -5980,7 +6162,7 @@ async def _dispatch_task_new_command(source_message: Message, actor: Optional[Us
5980
6162
  await dp.feed_update(bot_instance, update)
5981
6163
 
5982
6164
 
5983
- @router.message(F.text == WORKER_CREATE_TASK_BUTTON_TEXT)
6165
+ @router.message(F.text.in_(WORKER_CREATE_TASK_BUTTON_TEXT_SET))
5984
6166
  async def on_task_create_button(message: Message, state: FSMContext) -> None:
5985
6167
  await state.clear()
5986
6168
  try:
master.py CHANGED
@@ -2163,8 +2163,21 @@ async def _notify_restart_success(bot: Bot) -> None:
2163
2163
  except Exception as exc:
2164
2164
  log.error("Failed to send restart successful notification: %s", exc, extra={"chat": chat_id})
2165
2165
  else:
2166
- # After a successful restart, the project list will no longer be included to avoid extra noise during high-frequency restarts.
2166
+ # Restart succeeded; notify admins and push the refreshed project overview for quick status check.
2167
2167
  log.info("Restart successful notification has been sent", extra={"chat": chat_id, "duration": restart_duration})
2168
+ try:
2169
+ manager = await _ensure_manager()
2170
+ except RuntimeError as exc: # pragma: no cover - defensive guard if startup order changes
2171
+ log.warning("Manager unavailable when trying to push project overview: %s", exc)
2172
+ else:
2173
+ try:
2174
+ await _send_projects_overview_to_chat(bot, chat_id, manager)
2175
+ except Exception as exc: # pragma: no cover - avoid crashing startup hook
2176
+ log.error(
2177
+ "Failed to send project overview after restart notification: %s",
2178
+ exc,
2179
+ extra={"chat": chat_id},
2180
+ )
2168
2181
  finally:
2169
2182
  candidates = (signal_path, RESTART_SIGNAL_PATH, *LEGACY_RESTART_SIGNAL_PATHS)
2170
2183
  for candidate in candidates:
@@ -2198,7 +2211,8 @@ async def _process_restart_request(
2198
2211
 
2199
2212
  lock = _ensure_restart_lock()
2200
2213
  async with lock:
2201
- global_restart_in_progress
2214
+ # Use global so the restart flag is shared across concurrent handlers
2215
+ global _restart_in_progress
2202
2216
  if _restart_in_progress:
2203
2217
  await message.answer("A restart request is already being executed, please try again later. ")
2204
2218
  return
@@ -2258,7 +2272,8 @@ async def cmd_start(message: Message) -> None:
2258
2272
 
2259
2273
  async def _perform_restart(message: Message, start_script: Path) -> None:
2260
2274
  """Asynchronous execution ./start.sh,If it fails, roll back the mark and notify the administrator """
2261
- global_restart_in_progress
2275
+ # Use global so restart flag resets affect the module-level state
2276
+ global _restart_in_progress
2262
2277
  lock = _ensure_restart_lock()
2263
2278
  bot = message.bot
2264
2279
  chat_id = message.chat.id