vibego 1.0.2__py3-none-any.whl → 1.0.11__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 +345 -121
- 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.11.dist-info}/METADATA +10 -20
- {vibego-1.0.2.dist-info → vibego-1.0.11.dist-info}/RECORD +16 -15
- vibego_cli/__init__.py +1 -1
- {vibego-1.0.2.dist-info → vibego-1.0.11.dist-info}/WHEEL +0 -0
- {vibego-1.0.2.dist-info → vibego-1.0.11.dist-info}/entry_points.txt +0 -0
- {vibego-1.0.2.dist-info → vibego-1.0.11.dist-info}/licenses/LICENSE +0 -0
- {vibego-1.0.2.dist-info → vibego-1.0.11.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."
|
|
@@ -2335,6 +2394,9 @@ MODEL_PUSH_SUPPLEMENT_STATUSES: set[str] = {
|
|
|
2335
2394
|
"research",
|
|
2336
2395
|
"test",
|
|
2337
2396
|
}
|
|
2397
|
+
PUSH_MODEL_SUPPLEMENT_IN_PROGRESS_TEXT = (
|
|
2398
|
+
"A supplementary description prompt is already active. Please respond or tap Skip/Cancel."
|
|
2399
|
+
)
|
|
2338
2400
|
|
|
2339
2401
|
SUMMARY_COMMAND_PREFIX = "/task_summary_request_"
|
|
2340
2402
|
SUMMARY_COMMAND_ALIASES: tuple[str, ...] = (
|
|
@@ -3010,6 +3072,9 @@ async def _build_history_context_for_model(task_id: str) -> tuple[str, int]:
|
|
|
3010
3072
|
return "\n".join(trimmed_lines), len(trimmed_lines)
|
|
3011
3073
|
|
|
3012
3074
|
|
|
3075
|
+
SKIPPED_TASK_HISTORY_ACTIONS: set[str] = {"push_model", "summary_request"}
|
|
3076
|
+
|
|
3077
|
+
|
|
3013
3078
|
async def _log_task_action(
|
|
3014
3079
|
task_id: str,
|
|
3015
3080
|
*,
|
|
@@ -3023,11 +3088,20 @@ async def _log_task_action(
|
|
|
3023
3088
|
) -> None:
|
|
3024
3089
|
"""Encapsulate task event writing and record logs when exceptions occur to avoid interrupting the main process."""
|
|
3025
3090
|
|
|
3091
|
+
action_token = (action or "").strip()
|
|
3092
|
+
if action_token in SKIPPED_TASK_HISTORY_ACTIONS:
|
|
3093
|
+
worker_log.debug(
|
|
3094
|
+
"Skipped logging task action in history: task_id=%s action=%s",
|
|
3095
|
+
task_id,
|
|
3096
|
+
action_token,
|
|
3097
|
+
extra=_session_extra(),
|
|
3098
|
+
)
|
|
3099
|
+
return
|
|
3026
3100
|
data_payload: Optional[Dict[str, Any]]
|
|
3027
3101
|
if payload is None:
|
|
3028
|
-
data_payload = {"action":
|
|
3102
|
+
data_payload = {"action": action_token}
|
|
3029
3103
|
else:
|
|
3030
|
-
data_payload = {"action":
|
|
3104
|
+
data_payload = {"action": action_token, **payload}
|
|
3031
3105
|
try:
|
|
3032
3106
|
await TASK_SERVICE.log_task_event(
|
|
3033
3107
|
task_id,
|
|
@@ -4298,29 +4372,12 @@ async def _log_model_reply_event(
|
|
|
4298
4372
|
session_path: Path,
|
|
4299
4373
|
event_offset: int,
|
|
4300
4374
|
) -> None:
|
|
4301
|
-
"""
|
|
4375
|
+
"""Model replies are no longer persisted to history."""
|
|
4302
4376
|
|
|
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
|
-
)
|
|
4377
|
+
worker_log.debug(
|
|
4378
|
+
"Skipping history write for model reply",
|
|
4379
|
+
extra={"task_id": task_id, "session": str(session_path)},
|
|
4380
|
+
)
|
|
4324
4381
|
|
|
4325
4382
|
|
|
4326
4383
|
async def _maybe_finalize_summary(
|
|
@@ -4792,60 +4849,79 @@ async def _ensure_session_watcher(chat_id: int) -> Optional[Path]:
|
|
|
4792
4849
|
|
|
4793
4850
|
target_cwd = CODEX_WORKDIR or None
|
|
4794
4851
|
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4852
|
+
lock_session = _read_session_lock_path()
|
|
4853
|
+
lock_required = _is_session_lock_enforced()
|
|
4854
|
+
if lock_session is not None:
|
|
4855
|
+
if session_path is None or session_path != lock_session:
|
|
4798
4856
|
worker_log.info(
|
|
4799
|
-
"[session-map] chat=%s
|
|
4857
|
+
"[session-map] chat=%s use lock session %s",
|
|
4800
4858
|
chat_id,
|
|
4801
|
-
|
|
4802
|
-
extra=_session_extra(path=
|
|
4859
|
+
lock_session,
|
|
4860
|
+
extra=_session_extra(path=lock_session),
|
|
4803
4861
|
)
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
"
|
|
4811
|
-
chat_id,
|
|
4812
|
-
session_path,
|
|
4813
|
-
extra=_session_extra(path=session_path),
|
|
4862
|
+
_sync_pointer_with_lock(pointer_path, lock_session)
|
|
4863
|
+
session_path = lock_session
|
|
4864
|
+
else:
|
|
4865
|
+
if lock_required:
|
|
4866
|
+
worker_log.error(
|
|
4867
|
+
"[session-lock] Session lock required but missing during watcher ensure",
|
|
4868
|
+
extra={"chat": chat_id, "lock_file": SESSION_LOCK_FILE_PATH or "-"},
|
|
4814
4869
|
)
|
|
4870
|
+
return None
|
|
4871
|
+
if session_path is None and pointer_path is not None:
|
|
4872
|
+
session_path = _read_pointer_path(pointer_path)
|
|
4873
|
+
if session_path is not None:
|
|
4874
|
+
worker_log.info(
|
|
4875
|
+
"[session-map] chat=%s pointer -> %s",
|
|
4876
|
+
chat_id,
|
|
4877
|
+
session_path,
|
|
4878
|
+
extra=_session_extra(path=session_path),
|
|
4879
|
+
)
|
|
4880
|
+
if session_path is None and pointer_path is not None:
|
|
4881
|
+
latest = _find_latest_rollout_for_cwd(pointer_path, target_cwd)
|
|
4882
|
+
if latest is not None:
|
|
4883
|
+
session_path = latest
|
|
4884
|
+
_update_pointer(pointer_path, latest)
|
|
4885
|
+
worker_log.info(
|
|
4886
|
+
"[session-map] chat=%s locate latest rollout %s",
|
|
4887
|
+
chat_id,
|
|
4888
|
+
session_path,
|
|
4889
|
+
extra=_session_extra(path=session_path),
|
|
4890
|
+
)
|
|
4815
4891
|
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4892
|
+
if pointer_path is not None and _is_claudecode_model():
|
|
4893
|
+
fallback = _find_latest_claudecode_rollout(pointer_path)
|
|
4894
|
+
if fallback is not None and fallback != session_path:
|
|
4895
|
+
session_path = fallback
|
|
4896
|
+
_update_pointer(pointer_path, session_path)
|
|
4897
|
+
worker_log.info(
|
|
4898
|
+
"[session-map] chat=%s resume ClaudeCode session %s",
|
|
4899
|
+
chat_id,
|
|
4900
|
+
session_path,
|
|
4901
|
+
extra=_session_extra(path=session_path),
|
|
4902
|
+
)
|
|
4827
4903
|
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4904
|
+
if session_path is None and pointer_path is not None:
|
|
4905
|
+
session_path = await _await_session_path(pointer_path, target_cwd)
|
|
4906
|
+
if session_path is not None:
|
|
4907
|
+
_update_pointer(pointer_path, session_path)
|
|
4908
|
+
worker_log.info(
|
|
4909
|
+
"[session-map] chat=%s bind fresh session %s",
|
|
4910
|
+
chat_id,
|
|
4911
|
+
session_path,
|
|
4912
|
+
extra=_session_extra(path=session_path),
|
|
4913
|
+
)
|
|
4914
|
+
if session_path is None and pointer_path is not None and _is_claudecode_model():
|
|
4915
|
+
fallback = _find_latest_claudecode_rollout(pointer_path)
|
|
4916
|
+
if fallback is not None:
|
|
4917
|
+
session_path = fallback
|
|
4918
|
+
_update_pointer(pointer_path, session_path)
|
|
4919
|
+
worker_log.info(
|
|
4920
|
+
"[session-map] chat=%s fallback bind ClaudeCode session %s",
|
|
4921
|
+
chat_id,
|
|
4922
|
+
session_path,
|
|
4923
|
+
extra=_session_extra(path=session_path),
|
|
4924
|
+
)
|
|
4849
4925
|
|
|
4850
4926
|
if session_path is None:
|
|
4851
4927
|
worker_log.warning(
|
|
@@ -5268,6 +5344,115 @@ async def _watch_and_notify(chat_id: int, session_path: Path,
|
|
|
5268
5344
|
)
|
|
5269
5345
|
|
|
5270
5346
|
|
|
5347
|
+
_SESSION_LOCK_CACHE_MTIME: Optional[float] = None
|
|
5348
|
+
_SESSION_LOCK_CACHE_VALUE: Optional[Path] = None
|
|
5349
|
+
|
|
5350
|
+
|
|
5351
|
+
def _session_lock_file() -> Optional[Path]:
|
|
5352
|
+
if not SESSION_LOCK_FILE_PATH:
|
|
5353
|
+
return None
|
|
5354
|
+
return resolve_path(SESSION_LOCK_FILE_PATH)
|
|
5355
|
+
|
|
5356
|
+
|
|
5357
|
+
def _is_session_lock_enforced() -> bool:
|
|
5358
|
+
"""Return True when the worker must rely on the captured session lock."""
|
|
5359
|
+
|
|
5360
|
+
return SESSION_LOCK_REQUIRED and bool(SESSION_LOCK_FILE_PATH)
|
|
5361
|
+
|
|
5362
|
+
|
|
5363
|
+
def _session_lock_missing_message() -> str:
|
|
5364
|
+
"""Build a human-readable error when the session lock is missing."""
|
|
5365
|
+
|
|
5366
|
+
target = SESSION_LOCK_FILE_PATH or "session_lock.json"
|
|
5367
|
+
return (
|
|
5368
|
+
"当前 worker 未检测到会话锁,无法定位本项目独占的模型会话。\n"
|
|
5369
|
+
f"缺失的锁文件:{target}\n"
|
|
5370
|
+
"请在对应项目目录重新执行 scripts/run_bot.sh(或等效启动脚本)以捕获新的 tmux 会话。"
|
|
5371
|
+
)
|
|
5372
|
+
|
|
5373
|
+
|
|
5374
|
+
def _read_session_lock_path() -> Optional[Path]:
|
|
5375
|
+
"""Read the persisted session lock and return the rollout path when valid."""
|
|
5376
|
+
|
|
5377
|
+
lock_file = _session_lock_file()
|
|
5378
|
+
if lock_file is None:
|
|
5379
|
+
return None
|
|
5380
|
+
global _SESSION_LOCK_CACHE_MTIME, _SESSION_LOCK_CACHE_VALUE
|
|
5381
|
+
|
|
5382
|
+
try:
|
|
5383
|
+
stat = lock_file.stat()
|
|
5384
|
+
except FileNotFoundError:
|
|
5385
|
+
_SESSION_LOCK_CACHE_MTIME = None
|
|
5386
|
+
_SESSION_LOCK_CACHE_VALUE = None
|
|
5387
|
+
return None
|
|
5388
|
+
|
|
5389
|
+
mtime = stat.st_mtime
|
|
5390
|
+
if _SESSION_LOCK_CACHE_MTIME == mtime and _SESSION_LOCK_CACHE_VALUE is not None:
|
|
5391
|
+
return _SESSION_LOCK_CACHE_VALUE
|
|
5392
|
+
|
|
5393
|
+
try:
|
|
5394
|
+
raw = lock_file.read_text(encoding="utf-8")
|
|
5395
|
+
except OSError:
|
|
5396
|
+
_SESSION_LOCK_CACHE_MTIME = mtime
|
|
5397
|
+
_SESSION_LOCK_CACHE_VALUE = None
|
|
5398
|
+
return None
|
|
5399
|
+
try:
|
|
5400
|
+
payload = json.loads(raw)
|
|
5401
|
+
except json.JSONDecodeError:
|
|
5402
|
+
worker_log.warning(
|
|
5403
|
+
"[session-lock] Invalid JSON payload",
|
|
5404
|
+
extra={"lock": str(lock_file)},
|
|
5405
|
+
)
|
|
5406
|
+
_SESSION_LOCK_CACHE_MTIME = mtime
|
|
5407
|
+
_SESSION_LOCK_CACHE_VALUE = None
|
|
5408
|
+
return None
|
|
5409
|
+
|
|
5410
|
+
session_raw = payload.get("session_path")
|
|
5411
|
+
if not isinstance(session_raw, str) or not session_raw.strip():
|
|
5412
|
+
_SESSION_LOCK_CACHE_MTIME = mtime
|
|
5413
|
+
_SESSION_LOCK_CACHE_VALUE = None
|
|
5414
|
+
return None
|
|
5415
|
+
|
|
5416
|
+
rollout = resolve_path(session_raw.strip())
|
|
5417
|
+
if not rollout.exists():
|
|
5418
|
+
worker_log.warning(
|
|
5419
|
+
"[session-lock] Recorded session file is missing",
|
|
5420
|
+
extra={"session": str(rollout)},
|
|
5421
|
+
)
|
|
5422
|
+
_SESSION_LOCK_CACHE_MTIME = mtime
|
|
5423
|
+
_SESSION_LOCK_CACHE_VALUE = None
|
|
5424
|
+
return None
|
|
5425
|
+
|
|
5426
|
+
tmux_name = payload.get("tmux_session")
|
|
5427
|
+
if tmux_name and isinstance(tmux_name, str) and tmux_name.strip() and tmux_name.strip() != TMUX_SESSION:
|
|
5428
|
+
worker_log.info(
|
|
5429
|
+
"[session-lock] tmux mismatch, ignoring lock",
|
|
5430
|
+
extra={"lock_session": tmux_name.strip(), "tmux": TMUX_SESSION},
|
|
5431
|
+
)
|
|
5432
|
+
_SESSION_LOCK_CACHE_MTIME = mtime
|
|
5433
|
+
_SESSION_LOCK_CACHE_VALUE = None
|
|
5434
|
+
return None
|
|
5435
|
+
|
|
5436
|
+
_SESSION_LOCK_CACHE_MTIME = mtime
|
|
5437
|
+
_SESSION_LOCK_CACHE_VALUE = rollout
|
|
5438
|
+
return rollout
|
|
5439
|
+
|
|
5440
|
+
|
|
5441
|
+
def _sync_pointer_with_lock(pointer: Optional[Path], lock_path: Path) -> None:
|
|
5442
|
+
"""Ensure pointer.txt matches the locked session path."""
|
|
5443
|
+
|
|
5444
|
+
if pointer is None:
|
|
5445
|
+
return
|
|
5446
|
+
target = str(lock_path)
|
|
5447
|
+
try:
|
|
5448
|
+
current = pointer.read_text(encoding="utf-8").strip()
|
|
5449
|
+
except OSError:
|
|
5450
|
+
current = ""
|
|
5451
|
+
if current == target:
|
|
5452
|
+
return
|
|
5453
|
+
_update_pointer(pointer, lock_path)
|
|
5454
|
+
|
|
5455
|
+
|
|
5271
5456
|
def _read_pointer_path(pointer: Path) -> Optional[Path]:
|
|
5272
5457
|
try:
|
|
5273
5458
|
raw = pointer.read_text(encoding="utf-8").strip()
|
|
@@ -5483,7 +5668,7 @@ def _format_plan_update(arguments: Any, *, event_timestamp: Optional[str]) -> Op
|
|
|
5483
5668
|
if not steps:
|
|
5484
5669
|
return None
|
|
5485
5670
|
|
|
5486
|
-
header = "
|
|
5671
|
+
header = "current task execution plan:"
|
|
5487
5672
|
body_parts = [header]
|
|
5488
5673
|
if lines:
|
|
5489
5674
|
body_parts.extend(lines)
|
|
@@ -5945,7 +6130,7 @@ async def on_task_list(message: Message) -> None:
|
|
|
5945
6130
|
await _handle_task_list_request(message)
|
|
5946
6131
|
|
|
5947
6132
|
|
|
5948
|
-
@router.message(F.text
|
|
6133
|
+
@router.message(F.text.in_(WORKER_MENU_BUTTON_TEXT_SET))
|
|
5949
6134
|
async def on_task_list_button(message: Message) -> None:
|
|
5950
6135
|
await _handle_task_list_request(message)
|
|
5951
6136
|
|
|
@@ -5980,7 +6165,7 @@ async def _dispatch_task_new_command(source_message: Message, actor: Optional[Us
|
|
|
5980
6165
|
await dp.feed_update(bot_instance, update)
|
|
5981
6166
|
|
|
5982
6167
|
|
|
5983
|
-
@router.message(F.text
|
|
6168
|
+
@router.message(F.text.in_(WORKER_CREATE_TASK_BUTTON_TEXT_SET))
|
|
5984
6169
|
async def on_task_create_button(message: Message, state: FSMContext) -> None:
|
|
5985
6170
|
await state.clear()
|
|
5986
6171
|
try:
|
|
@@ -6667,6 +6852,10 @@ async def on_task_push_model(callback: CallbackQuery, state: FSMContext) -> None
|
|
|
6667
6852
|
await callback.answer("Callback parameter error.", show_alert=True)
|
|
6668
6853
|
return
|
|
6669
6854
|
_, _, task_id = parts
|
|
6855
|
+
current_state = await state.get_state()
|
|
6856
|
+
existing_context: Dict[str, Any] = {}
|
|
6857
|
+
if current_state == TaskPushStates.waiting_supplement.state:
|
|
6858
|
+
existing_context = await state.get_data()
|
|
6670
6859
|
task = await TASK_SERVICE.get_task(task_id)
|
|
6671
6860
|
if task is None:
|
|
6672
6861
|
await callback.answer("Task does not exist", show_alert=True)
|
|
@@ -6677,10 +6866,20 @@ async def on_task_push_model(callback: CallbackQuery, state: FSMContext) -> None
|
|
|
6677
6866
|
actor = _actor_from_callback(callback)
|
|
6678
6867
|
chat_id = callback.message.chat.id if callback.message else callback.from_user.id
|
|
6679
6868
|
if task.status in MODEL_PUSH_SUPPLEMENT_STATUSES:
|
|
6869
|
+
origin_message = callback.message
|
|
6870
|
+
origin_message_id = origin_message.message_id if origin_message else None
|
|
6871
|
+
if (
|
|
6872
|
+
current_state == TaskPushStates.waiting_supplement.state
|
|
6873
|
+
and existing_context.get("task_id") == task_id
|
|
6874
|
+
and existing_context.get("origin_message_id") == origin_message_id
|
|
6875
|
+
):
|
|
6876
|
+
await callback.answer(PUSH_MODEL_SUPPLEMENT_IN_PROGRESS_TEXT)
|
|
6877
|
+
return
|
|
6680
6878
|
await state.clear()
|
|
6681
6879
|
await state.update_data(
|
|
6682
6880
|
task_id=task_id,
|
|
6683
|
-
origin_message=
|
|
6881
|
+
origin_message=origin_message,
|
|
6882
|
+
origin_message_id=origin_message_id,
|
|
6684
6883
|
chat_id=chat_id,
|
|
6685
6884
|
actor=actor,
|
|
6686
6885
|
)
|
|
@@ -6780,16 +6979,31 @@ async def on_task_push_model_fill(callback: CallbackQuery, state: FSMContext) ->
|
|
|
6780
6979
|
await callback.answer("Callback parameter error.", show_alert=True)
|
|
6781
6980
|
return
|
|
6782
6981
|
_, _, task_id = parts
|
|
6982
|
+
current_state = await state.get_state()
|
|
6983
|
+
existing_context: Dict[str, Any] = {}
|
|
6984
|
+
if current_state == TaskPushStates.waiting_supplement.state:
|
|
6985
|
+
existing_context = await state.get_data()
|
|
6783
6986
|
task = await TASK_SERVICE.get_task(task_id)
|
|
6784
6987
|
if task is None:
|
|
6785
6988
|
await state.clear()
|
|
6786
6989
|
await callback.answer("Task does not exist", show_alert=True)
|
|
6787
6990
|
return
|
|
6788
6991
|
actor = _actor_from_callback(callback)
|
|
6992
|
+
origin_message = callback.message
|
|
6993
|
+
origin_message_id = origin_message.message_id if origin_message else None
|
|
6994
|
+
if (
|
|
6995
|
+
current_state == TaskPushStates.waiting_supplement.state
|
|
6996
|
+
and existing_context.get("task_id") == task_id
|
|
6997
|
+
and existing_context.get("origin_message_id") == origin_message_id
|
|
6998
|
+
):
|
|
6999
|
+
await callback.answer(PUSH_MODEL_SUPPLEMENT_IN_PROGRESS_TEXT)
|
|
7000
|
+
return
|
|
7001
|
+
await state.clear()
|
|
6789
7002
|
await state.update_data(
|
|
6790
7003
|
task_id=task_id,
|
|
6791
|
-
origin_message=
|
|
6792
|
-
|
|
7004
|
+
origin_message=origin_message,
|
|
7005
|
+
origin_message_id=origin_message_id,
|
|
7006
|
+
chat_id=origin_message.chat.id if origin_message else callback.from_user.id,
|
|
6793
7007
|
actor=actor,
|
|
6794
7008
|
)
|
|
6795
7009
|
await state.set_state(TaskPushStates.waiting_supplement)
|
|
@@ -7959,10 +8173,14 @@ async def on_start(m: Message):
|
|
|
7959
8173
|
await m.answer(_format_env_issue_message())
|
|
7960
8174
|
|
|
7961
8175
|
@router.message(F.text)
|
|
7962
|
-
async def on_text(m: Message):
|
|
8176
|
+
async def on_text(m: Message, state: FSMContext | None = None):
|
|
7963
8177
|
# Automatically record chat when first received message_id to state document
|
|
7964
8178
|
_auto_record_chat_id(m.chat.id)
|
|
7965
8179
|
|
|
8180
|
+
current_state: Optional[str] = None
|
|
8181
|
+
if state is not None:
|
|
8182
|
+
current_state = await state.get_state()
|
|
8183
|
+
|
|
7966
8184
|
prompt = (m.text or "").strip()
|
|
7967
8185
|
if not prompt:
|
|
7968
8186
|
return await m.answer("Please enter a non-empty prompt word")
|
|
@@ -7972,6 +8190,12 @@ async def on_text(m: Message):
|
|
|
7972
8190
|
return
|
|
7973
8191
|
if prompt.startswith("/"):
|
|
7974
8192
|
return
|
|
8193
|
+
if current_state:
|
|
8194
|
+
worker_log.debug(
|
|
8195
|
+
"Suppressed model dispatch due to active wizard state",
|
|
8196
|
+
extra={**_session_extra(), "chat": m.chat.id, "state": current_state},
|
|
8197
|
+
)
|
|
8198
|
+
return
|
|
7975
8199
|
await _handle_prompt_dispatch(m, prompt)
|
|
7976
8200
|
|
|
7977
8201
|
|
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
|