github-actions[bot] commited on
Commit ·
6ec98e7
1
Parent(s): 84cd838
Auto-deploy from GitHub: 5756a0af1b11482bda3fe8c090ee34cbae223e3e
Browse files- app/services/worker.py +64 -2
app/services/worker.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
| 1 |
import asyncio
|
| 2 |
import json
|
| 3 |
import os
|
|
|
|
| 4 |
import shutil
|
|
|
|
| 5 |
from app.core.config import settings
|
| 6 |
from custom_logger import logger_config as logger
|
| 7 |
from app.db import crud
|
|
@@ -142,6 +144,61 @@ async def _install_opencode():
|
|
| 142 |
logger.info("✅ opencode installed successfully")
|
| 143 |
|
| 144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
async def _run_opencode(system_prompt: str, text: str) -> str:
|
| 146 |
if not shutil.which('opencode'):
|
| 147 |
await _install_opencode()
|
|
@@ -165,13 +222,15 @@ async def _run_opencode(system_prompt: str, text: str) -> str:
|
|
| 165 |
stderr_lines = []
|
| 166 |
|
| 167 |
async def _read_stream(stream, lines, label):
|
|
|
|
| 168 |
while True:
|
| 169 |
line = await stream.readline()
|
| 170 |
if not line:
|
| 171 |
break
|
| 172 |
decoded = line.decode(errors='replace').rstrip()
|
| 173 |
lines.append(decoded)
|
| 174 |
-
|
|
|
|
| 175 |
|
| 176 |
# A whole-book prompt (reconcile, the director pass) runs big-pickle for
|
| 177 |
# around five minutes; 300s killed those just as they were finishing. Give
|
|
@@ -195,5 +254,8 @@ async def _run_opencode(system_prompt: str, text: str) -> str:
|
|
| 195 |
stderr = '\n'.join(stderr_lines)
|
| 196 |
|
| 197 |
if proc.returncode != 0:
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
| 199 |
return stdout
|
|
|
|
| 1 |
import asyncio
|
| 2 |
import json
|
| 3 |
import os
|
| 4 |
+
import re
|
| 5 |
import shutil
|
| 6 |
+
import time
|
| 7 |
from app.core.config import settings
|
| 8 |
from custom_logger import logger_config as logger
|
| 9 |
from app.db import crud
|
|
|
|
| 144 |
logger.info("✅ opencode installed successfully")
|
| 145 |
|
| 146 |
|
| 147 |
+
# opencode's `--print-logs` emits one structured line per internal event, e.g.
|
| 148 |
+
# INFO 2026-07-22T17:23:10 +37ms service=bus type=message.part.delta publishing
|
| 149 |
+
# A single answer produces thousands of these, one per streamed token, which
|
| 150 |
+
# buried every other worker log. Parse them instead of echoing: real problems
|
| 151 |
+
# (WARN/ERROR) get their own line, the token firehose collapses into a single
|
| 152 |
+
# overwriting heartbeat, and a persistent summary is written at the end.
|
| 153 |
+
_OPENCODE_LOG_RE = re.compile(
|
| 154 |
+
r'^(?P<level>DEBUG|INFO|WARN|ERROR)\s+\S+\s+\S+\s+(?P<rest>.*)$'
|
| 155 |
+
)
|
| 156 |
+
_HEARTBEAT_INTERVAL = 1.0 # seconds between heartbeat repaints
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
class _OpencodeLog:
|
| 160 |
+
def __init__(self, label):
|
| 161 |
+
self.label = label
|
| 162 |
+
self.count = 0
|
| 163 |
+
self.started = time.monotonic()
|
| 164 |
+
self.last_beat = 0.0
|
| 165 |
+
|
| 166 |
+
def emit(self, line):
|
| 167 |
+
if not line:
|
| 168 |
+
return
|
| 169 |
+
self.count += 1
|
| 170 |
+
|
| 171 |
+
match = _OPENCODE_LOG_RE.match(line)
|
| 172 |
+
level = match.group('level') if match else None
|
| 173 |
+
detail = match.group('rest') if match else line
|
| 174 |
+
|
| 175 |
+
if level == 'ERROR':
|
| 176 |
+
logger.error(f"opencode {self.label}: {detail}")
|
| 177 |
+
return
|
| 178 |
+
if level == 'WARN':
|
| 179 |
+
logger.warning(f"opencode {self.label}: {detail}")
|
| 180 |
+
return
|
| 181 |
+
|
| 182 |
+
now = time.monotonic()
|
| 183 |
+
if now - self.last_beat < _HEARTBEAT_INTERVAL:
|
| 184 |
+
return
|
| 185 |
+
self.last_beat = now
|
| 186 |
+
elapsed = int(now - self.started)
|
| 187 |
+
logger.info(
|
| 188 |
+
f"opencode {self.label}: {self.count} lines / {elapsed}s | {_shorten(detail)}",
|
| 189 |
+
overwrite=True,
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
def flush(self):
|
| 193 |
+
elapsed = int(time.monotonic() - self.started)
|
| 194 |
+
logger.info(f"opencode {self.label}: done — {self.count} lines in {elapsed}s")
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def _shorten(text, width=100):
|
| 198 |
+
text = text.strip()
|
| 199 |
+
return text if len(text) <= width else f"{text[:width - 1]}…"
|
| 200 |
+
|
| 201 |
+
|
| 202 |
async def _run_opencode(system_prompt: str, text: str) -> str:
|
| 203 |
if not shutil.which('opencode'):
|
| 204 |
await _install_opencode()
|
|
|
|
| 222 |
stderr_lines = []
|
| 223 |
|
| 224 |
async def _read_stream(stream, lines, label):
|
| 225 |
+
log = _OpencodeLog(label)
|
| 226 |
while True:
|
| 227 |
line = await stream.readline()
|
| 228 |
if not line:
|
| 229 |
break
|
| 230 |
decoded = line.decode(errors='replace').rstrip()
|
| 231 |
lines.append(decoded)
|
| 232 |
+
log.emit(decoded)
|
| 233 |
+
log.flush()
|
| 234 |
|
| 235 |
# A whole-book prompt (reconcile, the director pass) runs big-pickle for
|
| 236 |
# around five minutes; 300s killed those just as they were finishing. Give
|
|
|
|
| 254 |
stderr = '\n'.join(stderr_lines)
|
| 255 |
|
| 256 |
if proc.returncode != 0:
|
| 257 |
+
# stderr can be thousands of suppressed event lines; the tail is where
|
| 258 |
+
# the actual failure is.
|
| 259 |
+
tail = '\n'.join(stderr.splitlines()[-20:])
|
| 260 |
+
raise RuntimeError(f"opencode failed ({proc.returncode}): {tail or 'unknown error'}")
|
| 261 |
return stdout
|