refactor: remove sqlite routing
This commit is contained in:
@@ -23,7 +23,7 @@ chat_id = 123456789
|
|||||||
|
|
||||||
Optional keys:
|
Optional keys:
|
||||||
|
|
||||||
- common: `bridge_db`, `allowed_chat_ids`, `startup_chat_ids`
|
- common: `allowed_chat_ids`, `startup_chat_ids`
|
||||||
- exec/resume: `startup_message`, `codex_cmd`, `codex_workspace`, `codex_exec_args`, `max_workers`, `codex_io_mode`, `codex_command_timeout_s`, `codex_no_child_timeout_s`
|
- exec/resume: `startup_message`, `codex_cmd`, `codex_workspace`, `codex_exec_args`, `max_workers`, `codex_io_mode`, `codex_command_timeout_s`, `codex_no_child_timeout_s`
|
||||||
|
|
||||||
## Option 1: exec/resume
|
## Option 1: exec/resume
|
||||||
@@ -46,11 +46,14 @@ Optional flags:
|
|||||||
- `--workdir PATH` (override `codex_workspace`)
|
- `--workdir PATH` (override `codex_workspace`)
|
||||||
- `--model NAME` (pass through to `codex exec`)
|
- `--model NAME` (pass through to `codex exec`)
|
||||||
|
|
||||||
|
To resume an existing thread without a database, reply with (or include) the session id shown at the end of the bot response:
|
||||||
|
|
||||||
|
`resume: \`019b66fc-64c2-7a71-81cd-081c504cfeb2\``
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
- `src/codex_telegram_bridge/constants.py`: limits and config path constants
|
- `src/codex_telegram_bridge/constants.py`: limits and config path constants
|
||||||
- `src/codex_telegram_bridge/config.py`: config loading and chat-id parsing helpers
|
- `src/codex_telegram_bridge/config.py`: config loading and chat-id parsing helpers
|
||||||
- `src/codex_telegram_bridge/exec_render.py`: renderers for codex exec JSONL events
|
- `src/codex_telegram_bridge/exec_render.py`: renderers for codex exec JSONL events
|
||||||
- `src/codex_telegram_bridge/rendering.py`: markdown rendering
|
- `src/codex_telegram_bridge/rendering.py`: markdown rendering
|
||||||
- `src/codex_telegram_bridge/routes.py`: sqlite routing store
|
|
||||||
- `src/codex_telegram_bridge/telegram_client.py`: Telegram Bot API client
|
- `src/codex_telegram_bridge/telegram_client.py`: Telegram Bot API client
|
||||||
- `src/codex_telegram_bridge/exec_bridge.py`: codex exec + resume bridge
|
- `src/codex_telegram_bridge/exec_bridge.py`: codex exec + resume bridge
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
@@ -21,7 +22,6 @@ from .config import load_telegram_config
|
|||||||
from .constants import TELEGRAM_HARD_LIMIT
|
from .constants import TELEGRAM_HARD_LIMIT
|
||||||
from .exec_render import ExecProgressRenderer, ExecRenderState, render_event_cli
|
from .exec_render import ExecProgressRenderer, ExecRenderState, render_event_cli
|
||||||
from .rendering import render_markdown
|
from .rendering import render_markdown
|
||||||
from .routes import RouteStore
|
|
||||||
from .telegram_client import TelegramClient
|
from .telegram_client import TelegramClient
|
||||||
|
|
||||||
# -------------------- Codex runner --------------------
|
# -------------------- Codex runner --------------------
|
||||||
@@ -343,7 +343,6 @@ def run(
|
|||||||
setup_file_logger(log_file if log_file else None)
|
setup_file_logger(log_file if log_file else None)
|
||||||
config = load_telegram_config()
|
config = load_telegram_config()
|
||||||
token = config["bot_token"]
|
token = config["bot_token"]
|
||||||
db_path = config.get("bridge_db", "./bridge_routes.sqlite3")
|
|
||||||
|
|
||||||
def _as_int_set(value: Any) -> set[int]:
|
def _as_int_set(value: Any) -> set[int]:
|
||||||
if isinstance(value, int):
|
if isinstance(value, int):
|
||||||
@@ -389,7 +388,6 @@ def run(
|
|||||||
extra_args.extend(["-c", "notify=[]"])
|
extra_args.extend(["-c", "notify=[]"])
|
||||||
|
|
||||||
bot = TelegramClient(token)
|
bot = TelegramClient(token)
|
||||||
store = RouteStore(db_path)
|
|
||||||
runner = CodexExecRunner(codex_cmd=codex_cmd, workspace=workspace, extra_args=extra_args)
|
runner = CodexExecRunner(codex_cmd=codex_cmd, workspace=workspace, extra_args=extra_args)
|
||||||
|
|
||||||
max_workers = config.get("max_workers")
|
max_workers = config.get("max_workers")
|
||||||
@@ -484,14 +482,6 @@ def run(
|
|||||||
thread_id = evt.get("thread_id")
|
thread_id = evt.get("thread_id")
|
||||||
if isinstance(thread_id, str) and thread_id:
|
if isinstance(thread_id, str) and thread_id:
|
||||||
session_box["id"] = thread_id
|
session_box["id"] = thread_id
|
||||||
if progress_id is not None:
|
|
||||||
store.link(
|
|
||||||
chat_id,
|
|
||||||
progress_id,
|
|
||||||
"exec",
|
|
||||||
thread_id,
|
|
||||||
meta={"workspace": workspace},
|
|
||||||
)
|
|
||||||
if progress_renderer.note_event(evt) and progress is not None:
|
if progress_renderer.note_event(evt) and progress is not None:
|
||||||
elapsed = time.monotonic() - started_at
|
elapsed = time.monotonic() - started_at
|
||||||
msg = progress_renderer.render_progress(elapsed)
|
msg = progress_renderer.render_progress(elapsed)
|
||||||
@@ -515,11 +505,9 @@ def run(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
_stop_background()
|
_stop_background()
|
||||||
err = _clamp_tg_text(f"Error:\n{e}")
|
err = _clamp_tg_text(f"Error:\n{e}")
|
||||||
route_id = session_box["id"] or resume_session or "unknown"
|
|
||||||
if progress_id is not None and len(err) <= TELEGRAM_TEXT_LIMIT:
|
if progress_id is not None and len(err) <= TELEGRAM_TEXT_LIMIT:
|
||||||
try:
|
try:
|
||||||
bot.edit_message_text(chat_id=chat_id, message_id=progress_id, text=err)
|
bot.edit_message_text(chat_id=chat_id, message_id=progress_id, text=err)
|
||||||
store.link(chat_id, progress_id, "exec", route_id, meta={"error": True})
|
|
||||||
log(
|
log(
|
||||||
"[handle] error "
|
"[handle] error "
|
||||||
f"chat_id={chat_id} user_msg_id={user_msg_id} "
|
f"chat_id={chat_id} user_msg_id={user_msg_id} "
|
||||||
@@ -534,8 +522,6 @@ def run(
|
|||||||
text=err,
|
text=err,
|
||||||
reply_to_message_id=user_msg_id,
|
reply_to_message_id=user_msg_id,
|
||||||
)
|
)
|
||||||
for m in sent_msgs:
|
|
||||||
store.link(chat_id, m["message_id"], "exec", route_id, meta={"error": True})
|
|
||||||
log(
|
log(
|
||||||
"[handle] error "
|
"[handle] error "
|
||||||
f"chat_id={chat_id} user_msg_id={user_msg_id} resume_session={resume_session!r} err={e}"
|
f"chat_id={chat_id} user_msg_id={user_msg_id} resume_session={resume_session!r} err={e}"
|
||||||
@@ -549,6 +535,7 @@ def run(
|
|||||||
elapsed = time.monotonic() - started_at
|
elapsed = time.monotonic() - started_at
|
||||||
status = "done" if saw_agent_message else "error"
|
status = "done" if saw_agent_message else "error"
|
||||||
final_md = progress_renderer.render_final(elapsed, answer, status=status)
|
final_md = progress_renderer.render_final(elapsed, answer, status=status)
|
||||||
|
final_md = final_md + f"\n\nresume: `{session_id}`"
|
||||||
final_text, final_entities = render_markdown(final_md)
|
final_text, final_entities = render_markdown(final_md)
|
||||||
can_edit_final = progress_id is not None and len(final_text) <= TELEGRAM_TEXT_LIMIT
|
can_edit_final = progress_id is not None and len(final_text) <= TELEGRAM_TEXT_LIMIT
|
||||||
|
|
||||||
@@ -558,8 +545,6 @@ def run(
|
|||||||
text=final_md,
|
text=final_md,
|
||||||
reply_to_message_id=user_msg_id,
|
reply_to_message_id=user_msg_id,
|
||||||
)
|
)
|
||||||
for m in sent_msgs:
|
|
||||||
store.link(chat_id, m["message_id"], "exec", session_id, meta={"workspace": workspace})
|
|
||||||
if progress_id is not None:
|
if progress_id is not None:
|
||||||
try:
|
try:
|
||||||
bot.delete_message(chat_id=chat_id, message_id=progress_id)
|
bot.delete_message(chat_id=chat_id, message_id=progress_id)
|
||||||
@@ -575,7 +560,6 @@ def run(
|
|||||||
text=final_text,
|
text=final_text,
|
||||||
entities=final_entities or None,
|
entities=final_entities or None,
|
||||||
)
|
)
|
||||||
store.link(chat_id, progress_id, "exec", session_id, meta={"workspace": workspace})
|
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"[handle] done "
|
"[handle] done "
|
||||||
@@ -632,23 +616,20 @@ def run(
|
|||||||
f"chat_id={chat_id} user_msg_id={user_msg_id} text={_one_line(text)}"
|
f"chat_id={chat_id} user_msg_id={user_msg_id} text={_one_line(text)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# If user replied to a bot message, route to that session
|
uuid_re = re.compile(
|
||||||
resume_session: Optional[str] = None
|
r"(?i)\\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\b"
|
||||||
r = msg.get("reply_to_message")
|
|
||||||
if r and "message_id" in r:
|
|
||||||
route = store.resolve(chat_id, r["message_id"])
|
|
||||||
if route and route.route_type == "exec":
|
|
||||||
resume_session = route.route_id
|
|
||||||
log(
|
|
||||||
"[telegram] resolved reply route "
|
|
||||||
f"chat_id={chat_id} bot_message_id={r['message_id']} session_id={resume_session!r}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
log(
|
|
||||||
"[telegram] reply has no exec route "
|
|
||||||
f"chat_id={chat_id} bot_message_id={r['message_id']}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _extract_session_id(value: Optional[str]) -> Optional[str]:
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
m = uuid_re.search(value)
|
||||||
|
return m.group(0) if m else None
|
||||||
|
|
||||||
|
resume_session = _extract_session_id(text)
|
||||||
|
r = msg.get("reply_to_message") or {}
|
||||||
|
resume_session = resume_session or _extract_session_id(r.get("text"))
|
||||||
|
|
||||||
pool.submit(handle, chat_id, user_msg_id, text, resume_session)
|
pool.submit(handle, chat_id, user_msg_id, text, resume_session)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sqlite3
|
|
||||||
import time
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
|
|
||||||
|
|
||||||
def _now_unix() -> int:
|
|
||||||
return int(time.time())
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Route:
|
|
||||||
route_type: str # "exec"
|
|
||||||
route_id: str # session_id
|
|
||||||
meta: Dict[str, Any]
|
|
||||||
|
|
||||||
|
|
||||||
class RouteStore:
|
|
||||||
"""
|
|
||||||
Stores mapping: (chat_id, bot_message_id) -> route
|
|
||||||
so Telegram replies can be routed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, path: str) -> None:
|
|
||||||
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
||||||
self._conn = sqlite3.connect(path, check_same_thread=False)
|
|
||||||
self._conn.execute("PRAGMA journal_mode=WAL;")
|
|
||||||
self._conn.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS routes (
|
|
||||||
chat_id INTEGER NOT NULL,
|
|
||||||
bot_message_id INTEGER NOT NULL,
|
|
||||||
route_type TEXT NOT NULL,
|
|
||||||
route_id TEXT NOT NULL,
|
|
||||||
meta_json TEXT,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
PRIMARY KEY (chat_id, bot_message_id)
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
self._conn.execute(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_routes_route_id ON routes(route_id);"
|
|
||||||
)
|
|
||||||
self._conn.commit()
|
|
||||||
|
|
||||||
def link(
|
|
||||||
self,
|
|
||||||
chat_id: int,
|
|
||||||
bot_message_id: int,
|
|
||||||
route_type: str,
|
|
||||||
route_id: str,
|
|
||||||
meta: Optional[Dict[str, Any]] = None,
|
|
||||||
) -> None:
|
|
||||||
meta_json = json.dumps(meta or {}, ensure_ascii=False)
|
|
||||||
self._conn.execute(
|
|
||||||
"""
|
|
||||||
INSERT OR REPLACE INTO routes(chat_id, bot_message_id, route_type, route_id, meta_json, created_at)
|
|
||||||
VALUES(?, ?, ?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(chat_id, bot_message_id, route_type, route_id, meta_json, _now_unix()),
|
|
||||||
)
|
|
||||||
self._conn.commit()
|
|
||||||
|
|
||||||
def resolve(self, chat_id: int, bot_message_id: int) -> Optional[Route]:
|
|
||||||
cur = self._conn.execute(
|
|
||||||
"""
|
|
||||||
SELECT route_type, route_id, meta_json
|
|
||||||
FROM routes
|
|
||||||
WHERE chat_id = ? AND bot_message_id = ?
|
|
||||||
""",
|
|
||||||
(chat_id, bot_message_id),
|
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
|
||||||
if not row:
|
|
||||||
return None
|
|
||||||
route_type, route_id, meta_json = row
|
|
||||||
try:
|
|
||||||
meta = json.loads(meta_json) if meta_json else {}
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
meta = {}
|
|
||||||
return Route(route_type=route_type, route_id=route_id, meta=meta)
|
|
||||||
@@ -116,23 +116,14 @@ class TelegramClient:
|
|||||||
rendered_text, entities = render_markdown(text)
|
rendered_text, entities = render_markdown(text)
|
||||||
limit = min(chunk_len, TELEGRAM_HARD_LIMIT)
|
limit = min(chunk_len, TELEGRAM_HARD_LIMIT)
|
||||||
if len(rendered_text) > limit:
|
if len(rendered_text) > limit:
|
||||||
suffix = "\n" + ELLIPSIS
|
# Keep a tail section to preserve the "resume: `...`" line at the end.
|
||||||
keep = max(0, limit - len(suffix))
|
# If we truncate, drop entities to avoid offset gymnastics.
|
||||||
rendered_text = rendered_text[:keep] + suffix
|
sep = "\n" + ELLIPSIS + "\n"
|
||||||
if entities:
|
tail_len = min(400, max(120, limit // 3))
|
||||||
trimmed: List[Dict[str, Any]] = []
|
tail = rendered_text[-tail_len:]
|
||||||
for ent in entities:
|
head_len = max(0, limit - len(sep) - len(tail))
|
||||||
start = int(ent["offset"])
|
rendered_text = rendered_text[:head_len] + sep + tail
|
||||||
length = int(ent["length"])
|
entities = None
|
||||||
if start >= keep:
|
|
||||||
continue
|
|
||||||
end = min(start + length, keep)
|
|
||||||
if end <= start:
|
|
||||||
continue
|
|
||||||
d = dict(ent)
|
|
||||||
d["length"] = end - start
|
|
||||||
trimmed.append(d)
|
|
||||||
entities = trimmed
|
|
||||||
|
|
||||||
msg = self.send_message(
|
msg = self.send_message(
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user