fix(pipelines): clear today's real cron errors + kill monitoring false alarms

Real errors fixed (all verified by manual run):
- price_monitor.py: shares None -> TypeError at L584 (now completes 3m7s,
  full 39-stock reassess + zone triggers + Dad push)
- market_insight.py: net_inflow None -> TypeError at L142 (now 0.3s, 5 insights)
- promote_candidates.py: add busy_timeout=30s (DB lock under concurrent writes)
- premarket_full_review.py: 12-dim analysis now detached background launch
  (was doomed by cron 120s script timeout no matter what)

Systemic:
- HERMES_CRON_SCRIPT_TIMEOUT=600 drop-in for both gateway services
  (fixes mofin_health SIGTERM, market_watch timeout, memory_guardian timeout)
- sync_profile_scripts.sh: re-hardlink deploy->profile scripts after every
  deploy (scp replaces files = new inode = broken hardlink = cron silently
  runs stale code; this caused promote to keep failing after my first fix)

Monitoring false-alarm fixes (the '花瓶' problem):
- mofin_health.py: legacy JSONs that migrated to DB (multi_tf_cache/
  macro_context/market/live_prices/price_history/macro_risk_state) no longer
  warn 'no readers'; marked as migrated
- NEW db_freshness section: real pipeline health from DB tables
  (mtf_cache 0.4h / macro_context_log 2h / market_snapshots 2h /
  live_prices 0.4h / price_events.json 0.4h — ALL HEALTHY)
- price_events freshness reads live JSON store (DB table is legacy)
- market.json placeholder created (13+ scripts have fallback paths)

Investigation notes: wiki-self-growth 03:04 key1 429 predates full key6
activation on default gateway; current 8642 verified on key6 and working.
Weekend 'Blocked' jobs verified fixed (vacuum_state_db passes).
This commit is contained in:
hmo
2026-07-20 17:30:15 +08:00
parent a40b97f5ca
commit 17305bed0b
20 changed files with 417 additions and 20 deletions
+9
View File
@@ -0,0 +1,9 @@
import json
d = json.load(open('/home/hmo/.hermes/auth.json'))
pool = d.get('credential_pool', {})
for name, entries in pool.items():
if 'custom' in name or 'ocg' in name.lower():
for e in entries:
print(f"{name}: label={e.get('label')} priority={e.get('priority')} "
f"last_status={e.get('last_status')} err_code={e.get('last_error_code')} "
f"req_count={e.get('request_count')}")
+11
View File
@@ -0,0 +1,11 @@
import json
d = json.load(open('/home/hmo/.hermes/cron/jobs.json'))
jobs = d if isinstance(d, list) else d.get('jobs', [])
for j in jobs:
if j.get('name') in ('wiki-self-growth', 'evolution-pulse', '大脑任务执行', '知识研究-日常', '梦境循环-知识库归并'):
print(j['name'])
print(' provider:', j.get('provider'), '| model:', j.get('model'), '| base_url:', j.get('base_url'))
print(' no_agent:', j.get('no_agent'), '| script:', j.get('script'))
print(' timeout:', j.get('timeout'), '| max_turns:', j.get('max_turns'))
print(' prompt head:', str(j.get('prompt'))[:120])
print()
+19
View File
@@ -0,0 +1,19 @@
import sqlite3, json
# price_events in web-dashboard db
c = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
try:
r = c.execute("SELECT MAX(created_at), COUNT(*) FROM price_events").fetchone()
print('web-dashboard mofin.db price_events:', r)
except Exception as e:
print('web-dashboard db err:', e)
c.close()
# price_events.json tail
try:
d = json.load(open('/home/hmo/web-dashboard/data/price_events.json'))
print('price_events.json type:', type(d).__name__, 'len:', len(d) if hasattr(d, '__len__') else '?')
items = d if isinstance(d, list) else d.get('events', [])
if items:
print('last event:', json.dumps(items[-1], ensure_ascii=False)[:250])
except Exception as e:
print('json err:', e)
+9
View File
@@ -0,0 +1,9 @@
import sqlite3
conn = sqlite3.connect('/home/hmo/MoFin/data/mofin.db')
for t in ('mtf_cache', 'macro_context_log', 'live_prices', 'price_events', 'market_snapshots'):
try:
cols = [c[1] for c in conn.execute(f"PRAGMA table_info({t})")]
cnt = conn.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
print(f"{t}: rows={cnt} cols={cols}")
except Exception as e:
print(f"{t}: ERROR {e}")
+29
View File
@@ -0,0 +1,29 @@
import json, os
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
out = []
for jf, prof in [('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json', 'pa'),
('/home/hmo/.hermes/cron/jobs.json', 'default')]:
d = json.load(open(jf))
jobs = d if isinstance(d, list) else d.get('jobs', [])
for j in jobs:
st = j.get('last_status')
en = j.get('enabled', True)
if st == 'error' or not en:
lr = str(j.get('last_run_at') or '?')[:19]
err = str(j.get('last_error') or '')[:300].replace('\n', ' | ')
out.append({
'profile': prof, 'name': j.get('name'), 'script': j.get('script'),
'no_agent': j.get('no_agent'), 'status': st, 'enabled': en,
'last_run': lr, 'schedule': j.get('schedule_display') or str(j.get('schedule')),
'error': err,
})
print(f"TOTAL problem jobs: {len(out)}\n")
for o in out:
flag = 'DISABLED' if not o['enabled'] else 'ERROR'
print(f"[{flag}] ({o['profile']}) {o['name']}")
print(f" script={o['script']} no_agent={o['no_agent']} sched={o['schedule']} last={o['last_run']}")
print(f" err: {o['error'][:250]}")
print()
+20
View File
@@ -0,0 +1,20 @@
import json
d = json.load(open('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json'))
jobs = d if isinstance(d, list) else d.get('jobs', [])
targets = ['价格监控-高频', '知微洞察生成', '候选股自动提拔-每30分', '健康监控数据采集-每15分', '盘前全量重评-自选退出']
for j in jobs:
if j.get('name') in targets:
print('='*70)
print(j['name'], '| last:', str(j.get('last_run_at'))[:19])
print(str(j.get('last_error'))[:1200])
print()
d2 = json.load(open('/home/hmo/.hermes/cron/jobs.json'))
jobs2 = d2 if isinstance(d2, list) else d2.get('jobs', [])
for j in jobs2:
if j.get('name') in ('市场数据采集', '记忆守卫-每日', 'wiki-self-growth', 'evolution-pulse', '大脑任务执行'):
print('='*70)
print('[default]', j['name'], '| last:', str(j.get('last_run_at'))[:19])
print(str(j.get('last_error'))[:800])
print()
+60
View File
@@ -0,0 +1,60 @@
import json, os, subprocess
from datetime import datetime
now = datetime.now()
print("=== 1. DATA FILES ===")
for f in ['multi_tf_cache.json', 'macro_context.json']:
p = f'/home/hmo/MoFin/data/{f}'
if os.path.exists(p):
mtime = datetime.fromtimestamp(os.path.getmtime(p))
h = (now - mtime).total_seconds() / 3600
print(f'{f}: {mtime} ({h:.0f}h ago)')
else:
print(f'{f}: MISSING')
pm = '/home/hmo/web-dashboard/data/market.json'
print(f'market.json: {"EXISTS" if os.path.exists(pm) else "MISSING"}')
print()
print("=== 2. CRON MISSING SCRIPTS in profile scripts dir ===")
d = json.load(open('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json'))
jobs = d if isinstance(d, list) else d.get('jobs', [])
sdir = '/home/hmo/.hermes/profiles/position-analyst/scripts'
exist = set(os.listdir(sdir))
for j in jobs:
s = j.get('script', '')
if s and s not in exist and j.get('no_agent'):
f = subprocess.run(['find', '/home/hmo/MoFin', '-name', s, '-not', '-path', '*/venv/*'],
capture_output=True, text=True, timeout=15)
locs = [l for l in f.stdout.splitlines() if l.strip()]
print(f"MISSING: {s} (job: {j.get('name')}) -> {locs[:2] if locs else 'NOT FOUND'}")
print()
print("=== 3. suggestions table ===")
r = subprocess.run(['grep', '-rn', 'suggestions', '/home/hmo/MoFin/deploy/profile-scripts/', '--include=*.py'],
capture_output=True, text=True, timeout=10)
for l in r.stdout.splitlines():
if 'suggestions' in l.lower():
print(l.strip())
# Check if any cron references it
r2 = subprocess.run(['python3', '-c', '''
import json
d = json.load(open("/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json"))
jobs = d if isinstance(d, list) else d.get("jobs", [])
for j in jobs:
if "suggest" in str(j):
print(j.get("name",""), j.get("script",""), j.get("prompt","")[:100])
'''], capture_output=True, text=True, timeout=5)
print(r2.stdout)
print()
print("=== 4. price_monitor newspaper ===")
r = subprocess.run(['grep', '-n', 'newspaper', '/home/hmo/MoFin/deploy/profile-scripts/price_monitor.py'], capture_output=True, text=True, timeout=5)
print(r.stdout[-500:] if len(r.stdout) > 500 else r.stdout)
print()
print("=== 5. Which crons generate multi_tf_cache / macro_context ===")
for j in jobs:
if j.get('name') and ('多周期' in j.get('name','') or '宏观' in j.get('name','') or 'multi' in j.get('name','').lower() or 'market' in j.get('name','').lower()):
print(f"{j.get('name')}: script={j.get('script','?')} status={j.get('last_status','?')} last_run={j.get('last_run_at','?')} err={str(j.get('last_error',''))[:80]}")
+47
View File
@@ -0,0 +1,47 @@
import json, os
# 1. Check data pipeline stagnation
print("=== DATA FILE AGES ===")
for f in ['multi_tf_cache.json', 'macro_context.json', 'market.json']:
p = f'/home/hmo/MoFin/data/{f}'
if os.path.exists(p):
sec = (os.path.getmtime(p))
from datetime import datetime
print(f' {f}: {datetime.fromtimestamp(sec)} ({int((os.path.getmtime(p))/3600)}h ago)')
else:
print(f' {f}: MISSING')
# 2. Cron scripts missing from profile scripts dir
print()
print("=== MISSING CRON SCRIPTS ===")
d = json.load(open('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json'))
jobs = d if isinstance(d, list) else d.get('jobs', [])
scripts_dir = '/home/hmo/.hermes/profiles/position-analyst/scripts'
existing = set(os.listdir(scripts_dir))
missing = []
for j in jobs:
s = j.get('script', '')
if s and s not in existing and j.get('no_agent'):
missing.append((j.get('name'), s))
print(f' MISSING: {s} (job: {j.get(\"name\",\"?\")})')
if not missing:
print(' (none missing)')
# 3. Find source locations for missing scripts
print()
print("=== FIND MISSING SCRIPTS IN MoFin ===")
for name, script in missing:
import subprocess
r = subprocess.run(['find', '/home/hmo/MoFin', '-name', script, '-not', '-path', '*/venv/*'],
capture_output=True, text=True, timeout=15)
found = [l for l in r.stdout.splitlines() if l.strip()]
print(f' {script}: {found if found else "NOT FOUND"}')
# 4. market.json generator
print()
print("=== market.json GENERATOR ===")
r = subprocess.run(['grep', '-rn', "'market.json'", '/home/hmo/MoFin/deploy/profile-scripts/', '/home/hmo/MoFin/scripts/'],
capture_output=True, text=True, timeout=10)
for l in r.stdout.splitlines():
if 'market.json' in l:
print(f' {l}')
+39
View File
@@ -0,0 +1,39 @@
import sqlite3, uuid, time
db = sqlite3.connect('/home/hmo/.hermes/kanban.db')
print("=== statuses ===")
for r in db.execute("SELECT status, COUNT(*) FROM tasks GROUP BY status"):
print(r)
print()
# a pending example if any
r = db.execute("SELECT id, title, status, assignee FROM tasks WHERE status NOT IN ('done','cancelled') LIMIT 5").fetchall()
for x in r:
print(x)
# create the card for zhiwei
tid = 't_' + uuid.uuid4().hex[:8]
now = int(time.time())
title = "12维分析管道已系统化 — 每日自动刷新,请知悉并验证"
body = """笑笑(Sisyphus)完成系统级修复,知微不用再做任何手动补评。
背景:老爸发现盘前全量重评只更新了技术参数,12维LLM深度分析(full_analysis)为空/陈旧。
已落地的系统改动(已全部部署到246并提交git):
1. premarket_full_review.py 新增 Step 1.5:每交易日08:10自动跑 batch_reassess.py --type holding --today14只持仓12维分析每日强制刷新
2. batch_reassess.py 升级:--type holding|watchlist|all 全覆盖;分析超20h视为过期自动重评;现金/总资产改为从 portfolio_summary 实时读取(不再硬编码);港股前缀修复(00700等5位代码)
3. 新增每日12:30 cron「自选12维分析补全-每日午间」(watchlist_12d_backfill.py)109只缺分析的自选股每日补全
4. 验证:300308 已生成1920字12维分析(信号=观望)并写入DB;当前正在后台跑14只持仓的全量补评(/tmp/holdings_12d_backfill.log)
需要知微做的:
- 验证今天开盘简报/盯盘里能正常引用最新12维分析
- 观察今日12:30自选补全任务是否正常触发
- 有问题在kanban回复或XMPP找笑笑"""
db.execute(
"INSERT INTO tasks (id, title, body, assignee, status, priority, created_by, created_at) VALUES (?,?,?,?,?,?,?,?)",
(tid, title, body, 'zhiwei', 'pending', 1, 'xxm', now))
db.commit()
print()
print('created:', tid)
for r in db.execute("SELECT id, title, status, assignee, created_by FROM tasks WHERE id=?", (tid,)):
print(r)
db.close()
+7
View File
@@ -0,0 +1,7 @@
import sqlite3
db = sqlite3.connect('/home/hmo/.hermes/kanban.db')
db.execute("UPDATE tasks SET status='ready' WHERE id='t_2ff55641'")
db.commit()
for r in db.execute("SELECT id, status, assignee FROM tasks WHERE id='t_2ff55641'"):
print(r)
db.close()
+13
View File
@@ -0,0 +1,13 @@
import sqlite3, uuid
from datetime import datetime
db_path = '/home/hmo/.hermes/kanban.db'
db = sqlite3.connect(db_path)
# inspect schema
schema = db.execute("SELECT sql FROM sqlite_master WHERE name='tasks'").fetchone()
print(schema[0] if schema else 'no tasks table')
print()
# recent rows to see id format and fields
for r in db.execute("SELECT id, title, status, assignee, created_by, created_at FROM tasks ORDER BY created_at DESC LIMIT 5"):
print(r)
db.close()
+24
View File
@@ -0,0 +1,24 @@
import json, urllib.request
msg = """[笑笑] 12维分析管道已系统化完成 ✅
你早上指出的缺口(盘前重评只有技术参数、12维LLM分析为空/陈旧)已从系统层面修复,不是手动缝补:
1️⃣ 盘前管道改造:每交易日 08:10 自动跑持仓12维分析(batch_reassess --type holding --today,每日强制刷新14只)
2️⃣ batch_reassess 升级:持仓/自选全覆盖、分析超20h自动重评、现金/总资产改从 portfolio_summary 实时读(之前硬编码的是几周前的旧值)、港股前缀修复
3️⃣ 自选补全:新增每日12:30 cron「自选12维分析补全」,109只缺分析的自选股每日自动补
4️⃣ 验证:300308 已生成1920字12维分析(观望)写入DB;此刻后台正在跑14只持仓全量补评
代码已提交并同步到246 repomerge 4dcfee81)。
已通过 kanban 通知知微(t_2ff55641),让她验证今日开盘简报和12:30自选任务。
另外凌晨的自愈巡检还修了:promote候选崩溃、DB锁、符号链接阻断、candidate_filter等问题,今天开盘应该全部正常。"""
payload = json.dumps({"to": "hmo@yoin.fun", "body": msg, "type": "chat"}).encode()
req = urllib.request.Request("http://127.0.0.1:5805/", data=payload,
headers={"Content-Type": "application/json"})
try:
resp = urllib.request.urlopen(req, timeout=10)
print("XMPP sent:", resp.read().decode()[:100])
except Exception as e:
print("XMPP fail:", e)
+16
View File
@@ -0,0 +1,16 @@
import sqlite3
from datetime import datetime
conn = sqlite3.connect('/home/hmo/MoFin/data/mofin.db')
for t, c in [('mtf_cache','updated_at'),('macro_context_log','created_at'),
('live_prices','updated_at'),('price_events','created_at'),('market_snapshots','created_at')]:
try:
row = conn.execute(f"SELECT MAX({c}) FROM {t}").fetchone()
print(f"{t}.{c} MAX = {row[0]!r}")
if row and row[0]:
try:
dt = datetime.fromisoformat(str(row[0]).replace('Z',''))
print(f" parsed OK: {dt}")
except Exception as pe:
print(f" PARSE FAIL: {pe}")
except Exception as e:
print(f"{t}: QUERY ERROR {e}")
+11
View File
@@ -0,0 +1,11 @@
import json
d = json.load(open('/home/hmo/web-dashboard/static/mofin_health.json'))
print('generated:', d['generated_at'])
print('db_freshness:')
for f in d.get('db_freshness', []):
print(f" {f['label']}({f['table']}): last={f['last_record']} age={f['age_hours']}h warn={f['warn']}")
print()
warned = [j['name'] for j in d['json_files'] if j.get('warn')]
print(f'json_files warned: {len(warned)}')
migrated = [j['name'] for j in d['json_files'] if j.get('migrated_to_db')]
print(f'migrated (no longer warned): {migrated}')