Бот: починить resume-вызов Codex CLI
This commit is contained in:
parent
3e04727022
commit
b166013707
@ -656,13 +656,33 @@ class ShinePyBotService:
|
|||||||
self.state["current_history_file"] = str(history_file)
|
self.state["current_history_file"] = str(history_file)
|
||||||
self._persist_state()
|
self._persist_state()
|
||||||
|
|
||||||
def _current_history_file_for_user(self, username: str) -> Path:
|
def _user_session_state(self, username: str) -> dict[str, Any]:
|
||||||
uname = normalize_username(username) or self.cfg.allowed_username
|
uname = normalize_username(username) or self.cfg.allowed_username
|
||||||
self._ensure_user_session(uname)
|
self._ensure_user_session(uname)
|
||||||
sessions = self.state.get("user_sessions") or {}
|
sessions = self.state.get("user_sessions") or {}
|
||||||
session = sessions.get(uname) or {}
|
session = sessions.get(uname)
|
||||||
|
if not isinstance(session, dict):
|
||||||
|
session = {}
|
||||||
|
sessions[uname] = session
|
||||||
|
return session
|
||||||
|
|
||||||
|
def _current_history_file_for_user(self, username: str) -> Path:
|
||||||
|
session = self._user_session_state(username)
|
||||||
return Path(session["current_history_file"])
|
return Path(session["current_history_file"])
|
||||||
|
|
||||||
|
def _codex_thread_id_for_user(self, username: str) -> str:
|
||||||
|
thread_id = (self._user_session_state(username).get("codex_thread_id") or "").strip()
|
||||||
|
return thread_id
|
||||||
|
|
||||||
|
def _set_codex_thread_id_for_user(self, username: str, thread_id: str) -> None:
|
||||||
|
session = self._user_session_state(username)
|
||||||
|
normalized = (thread_id or "").strip()
|
||||||
|
if normalized:
|
||||||
|
session["codex_thread_id"] = normalized
|
||||||
|
else:
|
||||||
|
session.pop("codex_thread_id", None)
|
||||||
|
self._persist_state()
|
||||||
|
|
||||||
def _create_new_history_file(self, reason: str, username: str) -> Path:
|
def _create_new_history_file(self, reason: str, username: str) -> Path:
|
||||||
ts = dt.datetime.now().strftime("%Y-%m-%d_%H%M%S")
|
ts = dt.datetime.now().strftime("%Y-%m-%d_%H%M%S")
|
||||||
rnd = "".join(random.choices(string.hexdigits.lower(), k=8))
|
rnd = "".join(random.choices(string.hexdigits.lower(), k=8))
|
||||||
@ -690,7 +710,12 @@ class ShinePyBotService:
|
|||||||
if not isinstance(sessions, dict):
|
if not isinstance(sessions, dict):
|
||||||
sessions = {}
|
sessions = {}
|
||||||
self.state["user_sessions"] = sessions
|
self.state["user_sessions"] = sessions
|
||||||
|
previous = sessions.get(uname) if isinstance(sessions.get(uname), dict) else {}
|
||||||
sessions[uname] = {"current_history_file": str(new_file)}
|
sessions[uname] = {"current_history_file": str(new_file)}
|
||||||
|
if reason != "command_new" and isinstance(previous, dict):
|
||||||
|
thread_id = (previous.get("codex_thread_id") or "").strip()
|
||||||
|
if thread_id:
|
||||||
|
sessions[uname]["codex_thread_id"] = thread_id
|
||||||
if uname == self.cfg.allowed_username:
|
if uname == self.cfg.allowed_username:
|
||||||
self.state["current_history_file"] = str(new_file)
|
self.state["current_history_file"] = str(new_file)
|
||||||
self._persist_state()
|
self._persist_state()
|
||||||
@ -926,7 +951,7 @@ class ShinePyBotService:
|
|||||||
text = (
|
text = (
|
||||||
f"Привет, {player_name}.\n"
|
f"Привет, {player_name}.\n"
|
||||||
"Можно задавать вопросы по проекту, просить анализ, идеи и подготовку готового ТЗ.\n"
|
"Можно задавать вопросы по проекту, просить анализ, идеи и подготовку готового ТЗ.\n"
|
||||||
"Команда /new начинает новую сессию и архивирует текущую историю."
|
"Команда /new начинает новую Codex-сессию и архивирует текущую историю."
|
||||||
)
|
)
|
||||||
reminder = self._task_center_counts_text(uname)
|
reminder = self._task_center_counts_text(uname)
|
||||||
if reminder:
|
if reminder:
|
||||||
@ -1449,7 +1474,7 @@ class ShinePyBotService:
|
|||||||
"/tasks — список ваших задач и предложений",
|
"/tasks — список ваших задач и предложений",
|
||||||
"/stop — остановить текущую задачу",
|
"/stop — остановить текущую задачу",
|
||||||
"/cancel <id|all> — удалить задачу по id (префикс) или все",
|
"/cancel <id|all> — удалить задачу по id (префикс) или все",
|
||||||
"/new — архивировать историю и начать новую",
|
"/new — архивировать историю и начать новую Codex-сессию",
|
||||||
"/help — эта справка",
|
"/help — эта справка",
|
||||||
]
|
]
|
||||||
if is_owner:
|
if is_owner:
|
||||||
@ -1680,9 +1705,31 @@ class ShinePyBotService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _run_codex(self, prompt: str, job: dict[str, Any]) -> str:
|
def _run_codex(self, prompt: str, job: dict[str, Any]) -> str:
|
||||||
|
username = job.get("username") or self.cfg.allowed_username
|
||||||
|
thread_id = self._codex_thread_id_for_user(username)
|
||||||
|
try:
|
||||||
|
return self._run_codex_once(prompt, job, thread_id=thread_id)
|
||||||
|
except RuntimeError as e:
|
||||||
|
if not thread_id or not self._is_missing_codex_session_error(str(e)):
|
||||||
|
raise
|
||||||
|
self._set_codex_thread_id_for_user(username, "")
|
||||||
|
self._append_history(
|
||||||
|
Path(job["history_file"]),
|
||||||
|
"system_event",
|
||||||
|
{
|
||||||
|
"event": "codex_thread_reset",
|
||||||
|
"reason": "missing_session",
|
||||||
|
"username": normalize_username(username),
|
||||||
|
"oldThreadId": thread_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return self._run_codex_once(prompt, job, thread_id="")
|
||||||
|
|
||||||
|
def _run_codex_once(self, prompt: str, job: dict[str, Any], *, thread_id: str) -> str:
|
||||||
output_lines: list[str] = []
|
output_lines: list[str] = []
|
||||||
job_id = str(job["id"])
|
job_id = str(job["id"])
|
||||||
job_num = job.get("num", "?")
|
job_num = job.get("num", "?")
|
||||||
|
username = job.get("username") or self.cfg.allowed_username
|
||||||
with tempfile.NamedTemporaryFile(prefix="shine-codex-last-message-", suffix=".txt", delete=False) as tmp:
|
with tempfile.NamedTemporaryFile(prefix="shine-codex-last-message-", suffix=".txt", delete=False) as tmp:
|
||||||
output_file = Path(tmp.name)
|
output_file = Path(tmp.name)
|
||||||
|
|
||||||
@ -1693,9 +1740,12 @@ class ShinePyBotService:
|
|||||||
"--json",
|
"--json",
|
||||||
"-C", str(self.cfg.codex_workdir),
|
"-C", str(self.cfg.codex_workdir),
|
||||||
"-o", str(output_file),
|
"-o", str(output_file),
|
||||||
prompt,
|
|
||||||
]
|
]
|
||||||
print(f"[py-bot] codex exec start job={job_id[:8]}", flush=True)
|
if thread_id:
|
||||||
|
cmd.extend(["resume", thread_id])
|
||||||
|
cmd.append(prompt)
|
||||||
|
mode = f"resume {thread_id}" if thread_id else "new"
|
||||||
|
print(f"[py-bot] codex exec start job={job_id[:8]} mode={mode}", flush=True)
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
stdin=subprocess.DEVNULL,
|
stdin=subprocess.DEVNULL,
|
||||||
@ -1714,10 +1764,14 @@ class ShinePyBotService:
|
|||||||
last_user_note_at = 0.0
|
last_user_note_at = 0.0
|
||||||
codex_started_at = time.time()
|
codex_started_at = time.time()
|
||||||
last_job_message_at = codex_started_at
|
last_job_message_at = codex_started_at
|
||||||
|
seen_thread_id = ""
|
||||||
|
|
||||||
def on_line(line: str) -> None:
|
def on_line(line: str) -> None:
|
||||||
nonlocal last_user_note, last_user_note_at, last_job_message_at
|
nonlocal last_user_note, last_user_note_at, last_job_message_at, seen_thread_id
|
||||||
output_lines.append(line)
|
output_lines.append(line)
|
||||||
|
current_thread_id = self._extract_codex_thread_id(line)
|
||||||
|
if current_thread_id:
|
||||||
|
seen_thread_id = current_thread_id
|
||||||
note = self._extract_codex_user_note(line)
|
note = self._extract_codex_user_note(line)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if note and note != last_user_note and now - last_user_note_at > 8:
|
if note and note != last_user_note and now - last_user_note_at > 8:
|
||||||
@ -1770,6 +1824,9 @@ class ShinePyBotService:
|
|||||||
tail = "\n".join(output_lines[-40:])
|
tail = "\n".join(output_lines[-40:])
|
||||||
raise RuntimeError(f"Codex exited with code {return_code}. Output tail:\n{tail}")
|
raise RuntimeError(f"Codex exited with code {return_code}. Output tail:\n{tail}")
|
||||||
|
|
||||||
|
if seen_thread_id and seen_thread_id != thread_id:
|
||||||
|
self._set_codex_thread_id_for_user(username, seen_thread_id)
|
||||||
|
|
||||||
if output_file.exists():
|
if output_file.exists():
|
||||||
answer = output_file.read_text(encoding="utf-8").strip()
|
answer = output_file.read_text(encoding="utf-8").strip()
|
||||||
try:
|
try:
|
||||||
@ -2829,6 +2886,35 @@ class ShinePyBotService:
|
|||||||
return line
|
return line
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_codex_thread_id(line: str) -> str:
|
||||||
|
s = (line or "").strip()
|
||||||
|
if not s.startswith("{"):
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
obj = json.loads(s)
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
if obj.get("type") != "thread.started":
|
||||||
|
return ""
|
||||||
|
thread_id = (obj.get("thread_id") or "").strip()
|
||||||
|
return thread_id
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_missing_codex_session_error(text: str) -> bool:
|
||||||
|
lowered = (text or "").lower()
|
||||||
|
markers = [
|
||||||
|
"session not found",
|
||||||
|
"conversation not found",
|
||||||
|
"thread not found",
|
||||||
|
"no session found",
|
||||||
|
"invalid session",
|
||||||
|
"unknown session",
|
||||||
|
"no conversation found",
|
||||||
|
"unknown thread",
|
||||||
|
]
|
||||||
|
return any(marker in lowered for marker in markers)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _format_duration(seconds: int) -> str:
|
def _format_duration(seconds: int) -> str:
|
||||||
seconds = max(0, seconds)
|
seconds = max(0, seconds)
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.191
|
client.version=1.2.192
|
||||||
server.version=1.2.180
|
server.version=1.2.181
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user