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 +299 -117
- master.py +18 -3
- scripts/master_healthcheck.py +34 -134
- scripts/requirements.txt +1 -0
- scripts/run_bot.sh +3 -0
- scripts/session_pointer_watch.py +265 -0
- scripts/start.sh +16 -9
- scripts/start_tmux_codex.sh +25 -0
- tasks/service.py +1 -46
- {vibego-1.0.2.dist-info → vibego-1.0.10.dist-info}/METADATA +10 -20
- {vibego-1.0.2.dist-info → vibego-1.0.10.dist-info}/RECORD +16 -15
- vibego_cli/__init__.py +1 -1
- {vibego-1.0.2.dist-info → vibego-1.0.10.dist-info}/WHEEL +0 -0
- {vibego-1.0.2.dist-info → vibego-1.0.10.dist-info}/entry_points.txt +0 -0
- {vibego-1.0.2.dist-info → vibego-1.0.10.dist-info}/licenses/LICENSE +0 -0
- {vibego-1.0.2.dist-info → vibego-1.0.10.dist-info}/top_level.txt +0 -0
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
|
|
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
|
-
|
|
1040
|
-
|
|
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
|
|
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
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
-
|
|
1824
|
-
|
|
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":
|
|
3099
|
+
data_payload = {"action": action_token}
|
|
3029
3100
|
else:
|
|
3030
|
-
data_payload = {"action":
|
|
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
|
-
"""
|
|
4372
|
+
"""Model replies are no longer persisted to history."""
|
|
4302
4373
|
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
"
|
|
4306
|
-
|
|
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
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
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
|
|
4854
|
+
"[session-map] chat=%s use lock session %s",
|
|
4800
4855
|
chat_id,
|
|
4801
|
-
|
|
4802
|
-
extra=_session_extra(path=
|
|
4856
|
+
lock_session,
|
|
4857
|
+
extra=_session_extra(path=lock_session),
|
|
4803
4858
|
)
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
"
|
|
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
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
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
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
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 = "
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|