Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions bot/vk_longpoll.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ def __init__(
self.seen_permissions: Dict[str, set] = {}
self.seen_questions: Dict[str, set] = {}

# Защита от бесконечного цикла auto-continue: session_id -> count
self.auto_continue_count: Dict[str, int] = {}
self.AUTO_CONTINUE_MAX = 5

# ---------- SSE callbacks ----------

def _register_sse_callbacks(self):
Expand Down Expand Up @@ -375,8 +379,18 @@ async def _on_session_idle(self, event_type: str, data: dict):
title = child_info.get("title", session_id[:12])
await self.vk.send_message(target, f"✅ Subagent завершён: {title}")
else:
# Для основной сессии — тихо (последний text.ended уже отправил ответ)
logger.debug(f"Session idle: {session_id}")
# Для основной сессии — пытаемся продолжить работу агента (с защитой от бесконечного цикла)
count = self.auto_continue_count.get(session_id, 0)
if count >= self.AUTO_CONTINUE_MAX:
logger.info(f"Session idle — max auto-continue ({self.AUTO_CONTINUE_MAX}) reached for {session_id}, stopping")
target = THINKING_PEER_ID if THINKING_PEER_ID else user_id
await self.vk.send_message(target, "✅ Агент завершил работу (достигнут лимит автопродолжений).")
return
self.auto_continue_count[session_id] = count + 1
logger.info(f"Session idle — auto-continue {self.auto_continue_count[session_id]}/{self.AUTO_CONTINUE_MAX} for {session_id}")
target = THINKING_PEER_ID if THINKING_PEER_ID else user_id
await self.vk.send_message(target, f"⏳ Агент на паузе, продолжаю… ({self.auto_continue_count[session_id]}/{self.AUTO_CONTINUE_MAX})")
await self.opencode_client.send_prompt(session_id, "Продолжи работу, выполни оставшиеся задачи из плана.")

async def _on_session_status(self, event_type: str, data: dict):
"""Обрабатывает изменение статуса сессии (session.status)"""
Expand Down Expand Up @@ -441,7 +455,20 @@ async def _on_any_event(self, event_type: str, data: dict):
"""Логирует все SSE события для отладки"""
if event_type in self._NOISY_EVENTS:
return
logger.debug(f"SSE event: {event_type} keys={list(data.keys()) if isinstance(data, dict) else '?'}")
# Логируем важные поля с реальными значениями
extra = ""
if event_type.endswith(".step.ended") and isinstance(data, dict):
finish = data.get("finish", "?")
extra = f" finish={finish}"
elif event_type == "session.status" and isinstance(data, dict):
status = data.get("status", {})
if isinstance(status, dict):
extra = f" status_type={status.get('type', '?')} status_message={status.get('message', '')}"
else:
extra = f" status={status}"
elif event_type == "session.idle" and isinstance(data, dict):
extra = f" sessionID={data.get('sessionID', '?')[:16]}"
logger.debug(f"SSE event: {event_type}{extra} keys={list(data.keys()) if isinstance(data, dict) else '?'}")

# ---------- Обработка разрешений ----------
def _format_permission_message(self, perm: dict) -> str:
Expand Down Expand Up @@ -765,6 +792,9 @@ async def _handle_user_message(self, user_id: int, message: dict):
# Регистрируем маппинг session -> user для маршрутизации SSE событий
self.session_to_user[session_id] = user_id

# Сброс счётчика автопродолжения при новом сообщении пользователя
self.auto_continue_count[session_id] = 0

# Обрабатываем аттачи
attachment_info = ""
if attachments:
Expand Down Expand Up @@ -846,6 +876,7 @@ async def _handle_restart_command(self, user_id: int, text: str):
if old_session_id and old_session_id in self.session_to_user:
del self.session_to_user[old_session_id]
self.session_to_user[new_session_id] = user_id
self.auto_continue_count[new_session_id] = 0

await self.vk.send_message(user_id, f"✅ Модель {model_info} загружена")

Expand Down Expand Up @@ -973,9 +1004,9 @@ async def _handle_new_session_command(self, user_id: int, text: str = ""):
logger.warning(f"/newsession: workdir not found: {workdir_path}")
await self.vk.send_message(user_id, f"❌ Директория не найдена: {workdir_path}")
else:
# Без аргументов — используем текущую рабочую директорию процесса
workdir = getCwd()
logger.info(f"/newsession: no workdir specified, using cwd: {workdir}")
# Без аргументов — родительская директория от bot/
workdir = SCRIPT_DIR.parent
logger.info(f"/newsession: no workdir specified, using parent of script dir: {workdir}")
await self._new_session(user_id, workdir=workdir)

async def _new_session(self, user_id: int, workdir: Path = None):
Expand Down
43 changes: 43 additions & 0 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,7 @@ export const layer = Layer.effect(
const ctx = yield* InstanceState.context
let structured: unknown
let step = 0
let autoContinueCount = 0
const session = yield* sessions.get(sessionID).pipe(Effect.orDie)

while (true) {
Expand Down Expand Up @@ -1178,6 +1179,48 @@ export const layer = Layer.effect(
callID: orphan.callID,
})
}
// AUTO-CONTINUE: Detect reasoning-only + stop without any text response or tool calls.
// This catches cases where the model stops mid-thought without producing output.
const isReasoningOnly = lastAssistantMsg && lastAssistant.finish === "stop" && lastAssistantMsg.parts.length > 0 &&
lastAssistantMsg.parts.every(
(p) => p.type === "reasoning" || p.type === "step-start" || p.type === "step-finish",
)

if (isReasoningOnly && autoContinueCount < 10) {
autoContinueCount++
yield* Effect.logInfo("auto-continue: reasoning-only stop, injecting continue message", {
"session.id": sessionID,
attempt: autoContinueCount,
})
const contMsgID = MessageID.ascending()
const contMsg = yield* sessions.updateMessage({
id: contMsgID,
role: "user",
sessionID,
time: { created: Date.now() },
agent: lastUser.agent,
model: lastUser.model,
})
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: contMsg.id,
sessionID,
type: "text",
metadata: { auto_continue: true },
synthetic: true,
text: "[Auto-continue] Your response was cut off. Please continue from where you left off.",
time: {
start: Date.now(),
end: Date.now(),
},
})
continue
} else if (isReasoningOnly) {
yield* Effect.logWarning("auto-continue: max attempts (10) reached, exiting loop", {
"session.id": sessionID,
})
}

yield* Effect.logInfo("exiting loop", { "session.id": sessionID })
break
}
Expand Down
Loading