fix(caching): include Kimi/Moonshot in OpenRouter prompt cache policy (#25970)

This commit is contained in:
zccyman 2026-05-15 08:15:31 +08:00 committed by zccyman
parent 43e566f77e
commit 3b857a35e7
7 changed files with 794 additions and 1 deletions

BIN
.dev-workflow/code-graph.db Normal file

Binary file not shown.

View file

@ -0,0 +1,74 @@
{
"template": "github-issue",
"current_phase": 13,
"completed_phases": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13
],
"issue_number": 27653,
"started_at": "2026-05-18T07:02:16.341710",
"last_advanced_at": "2026-05-18T07:15:27.942283",
"state": "completed",
"phase_evidence": {
"1": {
"evidence": "Issue #27653 scored 6/7, no competing PRs found across 5 keyword searches (history, history search, prompt reuse, slash command gateway), no duplicate markers, 0 comments. Feature request: add /history cmd with search & reuse for gateway platforms. Author: zccyman. Labels: type/feature, comp/gateway.",
"timestamp": "2026-05-18T07:02:41.657629"
},
"2": {
"evidence": "branch feat/history-gateway-27653 created from origin/main (4c46c35ed), git email set to 16263913+zccyman@users.noreply.github.com for CI attribution",
"timestamp": "2026-05-18T07:03:04.882165"
},
"3": {
"evidence": "Code understanding complete: 6 key files analyzed. /history is cli_only=True in commands.py L74. Gateway dispatch at run.py L6505-6760 uses canonical command chain. Session store has load_transcript() and search_messages() (FTS5). Feishu cards at feishu.py L1854 support interactive buttons with value callbacks. /retry at L9690 shows transcript reading pattern.",
"timestamp": "2026-05-18T07:04:41.647803"
},
"4": {
"evidence": "dwf_security_scan quick: all findings pre-existing in CI configs (no github, docker) and test fixtures. Feature adds read-only /history slash command that reads session store — no new code execution, no file writes, no credentials exposed. Zero risk.",
"timestamp": "2026-05-18T07:04:58.163837"
},
"5": {
"evidence": "Modified 2 files: hermes_cli/commands.py (removed cli_only=True, added args_hint=[keyword]) and gateway/run.py (added dispatch line for history + _handle_history_command handler (96 lines). Handler supports /history, /history <page>, /history <keyword>, paginated 5/page. Syntax check passed.",
"timestamp": "2026-05-18T07:11:15.389855"
},
"6": {
"evidence": "10 new tests in tests/gateway/test_history_command.py all PASSED (test_no_history, test_shows_recent_prompts_page_1, test_pagination_page_2, test_search_by_keyword, test_search_no_match, test_page_out_of_range_clamps_to_last_page, test_session_store_unavailable, test_filters_tool_and_assistant, test_empty_args_from_event, test_history_next_prev_navigation). Existing gateway tests (14) also PASSED — zero regressions.",
"timestamp": "2026-05-18T07:13:38.921925"
},
"7": {
"evidence": "dwf_review_run: 0 ERROR, 1 WARNING (pre-existing http://127.0.0.1 in commands.py L1107, NOT our change), 89 INFO (all pre-existing style/assert issues). Zero new findings across 3 files reviewed.",
"timestamp": "2026-05-18T07:13:58.765627"
},
"8": {
"evidence": "PR readiness 7/7: (1) attribution OK, (2) no new competitors, (3) diff +281/-2, (4) syntax OK, (5) 10 tests passed, (6) code review clean, (7) PR body drafted. Ready for PR creation.",
"timestamp": "2026-05-18T07:14:40.080604"
},
"9": {
"evidence": "Phase 10: Self-evolution completed - extracted 5 decisions, 5 pitfalls, knowledge graph updated. Phase 11: No upstream gap discoveries - this PR implements an issue we filed. Phase 12: Plugin self-improvement - dev-workflow plugin performed well, phase_tracker.py correctly blocked pre-push until Phase 9.",
"timestamp": "2026-05-18T07:15:20.667105"
},
"10": {
"evidence": "Phase 11: No upstream gap discoveries found. This PR directly implements the feature requested in the issue that we filed.",
"timestamp": "2026-05-18T07:15:23.003379"
},
"11": {
"evidence": "Phase 12: dev-workflow plugin performed well this cycle. phase_tracker.py pre-push hook correctly blocked Phase 8 push. Only minor issue: advancing multiple phases at once is not supported — must advance one at a time.",
"timestamp": "2026-05-18T07:15:25.464885"
},
"12": {
"evidence": "Phase 13: Cleanup complete. Branch feat/history-gateway-27653 pushed to fork. PR #27655 created at https://github.com/NousResearch/hermes-agent/pull/27655. Switching to main branch.",
"timestamp": "2026-05-18T07:15:27.942267"
}
},
"last_updated": "2026-05-18T07:15:30.558391"
}

View file

@ -1109,6 +1109,13 @@ def anthropic_prompt_cache_policy(
model_lower = eff_model.lower()
provider_lower = eff_provider.lower()
is_claude = "claude" in model_lower
# Kimi / Moonshot family via OpenRouter: same cache_control wire format
# as Claude on OpenRouter (envelope layout). Without this branch
# moonshotai/kimi-k2.6 falls through to (False, False), serving ~1%
# cache hits on 64K-token prompts and re-billing the full prompt on
# every turn. Observed within-turn progression with cache enabled:
# 1% → 67% → 84% → 97% (#25970).
is_kimi = "kimi" in model_lower or "moonshot" in model_lower
is_openrouter = base_url_host_matches(eff_base_url, "openrouter.ai")
# Nous Portal proxies to OpenRouter behind the scenes — identical
# OpenAI-wire envelope cache_control semantics. Treat it as an
@ -1122,7 +1129,7 @@ def anthropic_prompt_cache_policy(
if is_native_anthropic:
return True, True
if (is_openrouter or is_nous_portal) and is_claude:
if (is_openrouter or is_nous_portal) and (is_claude or is_kimi):
return True, False
# Nous Portal Qwen (e.g. qwen3.6-plus) takes the same envelope-layout
# cache_control path as Portal Claude. Portal proxies to OpenRouter

117
analyzed_issues.json Normal file
View file

@ -0,0 +1,117 @@
{
"27198": {
"title": "[Bug]: WhatsApp messages stuck at 1 tick — delivery ACK missing in addition to read receipts",
"url": "https://github.com/NousResearch/hermes-agent/issues/27198",
"seen_at": "2026-05-17T01:45:29.080613+00:00"
},
"27166": {
"title": "[Bug] Telegram DM topic response routed to All Messages after session split",
"url": "https://github.com/NousResearch/hermes-agent/issues/27166",
"seen_at": "2026-05-17T01:45:29.080852+00:00"
},
"27156": {
"title": "Session resume can replay prior assistant/tool context instead of answering exact-output prompt",
"url": "https://github.com/NousResearch/hermes-agent/issues/27156",
"seen_at": "2026-05-17T01:45:29.081027+00:00"
},
"27312": {
"title": "Browser tool should prefer packaged agent-browser native binary when npm shim needs unavailable node",
"url": "https://github.com/NousResearch/hermes-agent/issues/27312",
"seen_at": "2026-05-17T07:46:52.354983+00:00"
},
"27296": {
"title": "[Bug] delegate_task ignores delegation.base_url — subagent always inherits parent model",
"url": "https://github.com/NousResearch/hermes-agent/issues/27296",
"seen_at": "2026-05-17T07:46:52.355896+00:00"
},
"27295": {
"title": "[Bug] delegate_task ignores delegation.base_url — subagent always inherits parent model",
"url": "https://github.com/NousResearch/hermes-agent/issues/27295",
"seen_at": "2026-05-17T07:46:52.357442+00:00"
},
"27294": {
"title": "[Bug] delegate_task ignores delegation.base_url — subagent always inherits parent model",
"url": "https://github.com/NousResearch/hermes-agent/issues/27294",
"seen_at": "2026-05-17T07:46:52.358251+00:00"
},
"27282": {
"title": "[--tui] gateway exits mid-turn with stdin EOF (TUI closed the command pipe) — NOT byterover-related",
"url": "https://github.com/NousResearch/hermes-agent/issues/27282",
"seen_at": "2026-05-17T07:46:52.359345+00:00"
},
"27265": {
"title": "Discord long bot-to-bot handoffs can drop unmentioned continuation chunks",
"url": "https://github.com/NousResearch/hermes-agent/issues/27265",
"seen_at": "2026-05-17T07:46:52.360103+00:00"
},
"27230": {
"title": "[Bug]: Telegram gateway only acknowledges tool requests but never delivers final results",
"url": "https://github.com/NousResearch/hermes-agent/issues/27230",
"seen_at": "2026-05-17T07:46:52.361212+00:00"
},
"27430": {
"title": "hermes update fails web UI build when NODE_ENV=production — npm install omits devDependencies",
"url": "https://github.com/NousResearch/hermes-agent/issues/27430",
"seen_at": "2026-05-17T13:48:05.033485+00:00"
},
"27402": {
"title": "NameError: '_pool_may_recover_from_rate_limit' is not defined in conversation_loop.py",
"url": "https://github.com/NousResearch/hermes-agent/issues/27402",
"seen_at": "2026-05-17T13:48:05.071950+00:00"
},
"27383": {
"title": "[Bug]: change working directory for Telegram agent",
"url": "https://github.com/NousResearch/hermes-agent/issues/27383",
"seen_at": "2026-05-17T13:48:05.073121+00:00"
},
"27352": {
"title": "[Bug] Gateway approval wait is not resolved by normal user replies during pending approval",
"url": "https://github.com/NousResearch/hermes-agent/issues/27352",
"seen_at": "2026-05-17T13:48:05.074324+00:00"
},
"27345": {
"title": "[Bug]: API connection Fail with minimax provider in windows",
"url": "https://github.com/NousResearch/hermes-agent/issues/27345",
"seen_at": "2026-05-17T13:48:05.075138+00:00"
},
"27566": {
"title": "Context compression triggers every turn due to rough token estimate inflating last_prompt_tokens",
"url": "https://github.com/NousResearch/hermes-agent/issues/27566",
"seen_at": "2026-05-17T19:48:41.166309+00:00"
},
"27563": {
"title": "Python shutdown deadlock: worker thread blocks Py_FinalizeEx, process hangs forever after exit",
"url": "https://github.com/NousResearch/hermes-agent/issues/27563",
"seen_at": "2026-05-17T19:48:41.167190+00:00"
},
"27555": {
"title": "Bug: vision fallback_chain silently broken — wrong kwargs in _resolve_single_provider",
"url": "https://github.com/NousResearch/hermes-agent/issues/27555",
"seen_at": "2026-05-17T19:48:41.168199+00:00"
},
"27540": {
"title": "[Bug]: AIAgent() got multiple values for keyword argument 'model' — reproduces on second turn of session",
"url": "https://github.com/NousResearch/hermes-agent/issues/27540",
"seen_at": "2026-05-17T19:48:41.168926+00:00"
},
"27538": {
"title": "auxiliary.compression.provider ignored after switching model provider",
"url": "https://github.com/NousResearch/hermes-agent/issues/27538",
"seen_at": "2026-05-17T19:48:41.170045+00:00"
},
"27529": {
"title": "[feishu] Fix markdown table rendering: use post+tag:md instead of force-text workaround",
"url": "https://github.com/NousResearch/hermes-agent/issues/27529",
"seen_at": "2026-05-17T19:48:41.170928+00:00"
},
"27477": {
"title": "[Bug] Feishu 转发聊天记录无法被 Hermes 读取",
"url": "https://github.com/NousResearch/hermes-agent/issues/27477",
"seen_at": "2026-05-17T19:48:41.172039+00:00"
},
"27469": {
"title": "[feishu] Unified fix for markdown rendering: inbound escaping + GFM table → Card 2.0",
"url": "https://github.com/NousResearch/hermes-agent/issues/27469",
"seen_at": "2026-05-17T19:48:41.173084+00:00"
}
}

445
scripts/phase_tracker.py Normal file
View file

@ -0,0 +1,445 @@
#!/usr/bin/env python3
"""
Phase Tracker strict multi-template phase enforcement for upstream contributions.
Usage:
phase_tracker.py start <template> [--issue N]
phase_tracker.py current # show current phase
phase_tracker.py check <N> # exit 0 if on phase N, 1 if not
phase_tracker.py advance --evidence "描述" # submit evidence + advance
phase_tracker.py skip <N> # BLOCKED
phase_tracker.py complete # mark workflow done
phase_tracker.py abort # cancel workflow
phase_tracker.py phases # list templates + phases
State file: .dev-workflow/phase-tracker.json
GUARD RAILS:
- `advance` requires --evidence describing what was done (min 20 chars)
- Phase 1 (triage) blocks advance if competing PR was found but no decision recorded
- All evidence is stored and auditable
"""
import json
import os
import sys
STATE_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".dev-workflow")
STATE_FILE = os.path.join(STATE_DIR, "phase-tracker.json")
# Minimum evidence length to prevent empty/generic submissions
MIN_EVIDENCE_LENGTH = 20
TEMPLATES = {
"github-issue": {
"total": 13,
"PHASES": {
1: "triage", 2: "setup", 3: "understand", 4: "security",
5: "implement", 6: "test", 7: "review", 8: "preflight",
9: "pr", 10: "evolve", 11: "insight-feature-request",
12: "plugin-improve", 13: "cleanup",
},
"PHASE_NAMES": {
1: "🔍 Deep Triage — 检查竞争 PR、评分、分类",
2: "🌿 Branch Setup — 同步上游、创建分支",
3: "📖 Code Understanding — 读代码、追踪执行路径",
4: "🔒 Security Baseline — 安全基线扫描",
5: "✏️ Implementation — 写代码(小 diff",
6: "🧪 Test Suite — 全量测试 + 新增测试(必须全通过)",
7: "👀 Code Review — 多角色静态审查(零 ERROR",
8: "✅ PR Readiness Check — 7 项预提交检查",
9: "🚀 Create Pull Request — 提交 + 创建 PR",
10: "🧠 Self Evolution — 萃取经验,同步知识图谱",
11: "💡 Insight-Driven Feature Request — 发现上游缺口",
12: "🔧 Plugin Self-Improvement — 改进插件",
13: "🧹 Cleanup & Handoff — 清理分支,交接",
},
# What must the evidence describe for each phase?
"EVIDENCE_HINTS": {
1: "e.g. 'issue #N scored 6/7, no competing PR, author=X, labels=Y'",
2: "e.g. 'branch fix/xxx created from origin/main, fetched latest'",
3: "e.g. 'root cause: func X does Y instead of Z, traced via file A→B→C'",
4: "e.g. 'dwf_security_scan quick: 0 findings, text-only change' or 'scan clean'",
5: "e.g. 'modified files X, Y — +N/-M lines, syntax check passed'",
6: "e.g. 'N tests passed, 0 failed — new tests in test_X.py'",
7: "e.g. 'dwf_review_run: 0 ERROR, N WARNING — all reviewed'",
8: "e.g. 'attribution OK, no new competitor, diff +N/-M, 7/7 checks pass'",
9: "e.g. 'PR #N created: https://github.com/.../pull/N'",
10: "e.g. 'extracted 2 patterns: provider-flag-default, conservative-fallback'",
11: "e.g. 'no upstream gap found' or 'found gap: file X missing Y'",
12: "e.g. 'no plugin changes needed' or 'updated pr-review-patterns.md'",
13: "e.g. 'checked out main, branch deleted, state cleaned'",
},
},
"pr-review-response": {
"total": 12,
"PHASES": {
1: "scan-classify", 2: "simple-replies", 3: "worth-check",
4: "respond", 5: "implement-fixes", 6: "test",
7: "pre-push", 8: "commit-push", 9: "evolve",
10: "insight-feature-request", 11: "plugin-improve", 12: "cleanup",
},
"PHASE_NAMES": {
1: "📋 Scan & Classify — 扫描所有 PR按路线分类",
2: "📬 Simple Replies — 感谢/关闭重复/竞争 PR",
3: "💎 Worth Check — 重新评估 PR 是否值得继续",
4: "💬 Respond to Reviewers — 回复认可确定改动方向",
5: "✏️ Implement Changes — 按 reviewer 要求改代码",
6: "🧪 Test Suite — 全量测试(必须通过)",
7: "🔍 Pre-Push Re-check — 确认无新竞争/新评论",
8: "📤 Commit, Push & Follow-up — 推送+通知 reviewer",
9: "🧠 Self Evolution — 萃取经验,同步知识图谱",
10: "💡 Insight-Driven Feature Request — 发现上游缺口",
11: "🔧 Plugin Self-Improvement — 改进插件",
12: "🧹 Cleanup — 清理分支,记录待处理 PR",
},
"EVIDENCE_HINTS": {
1: "e.g. 'scanned N PRs: A=RouteA, B=RouteB, C=RouteC'",
2: "e.g. 'closed #N (duplicate), thanked #M (cherry-pick)'",
3: "e.g. 'PR #N worth keeping — reviewer raised valid point'",
4: "e.g. 'replied to reviewer X on PR #N, agreed to fix Y'",
5: "e.g. 'fixed Y per reviewer feedback, +N/-M lines'",
6: "e.g. 'N tests passed, 0 failed'",
7: "e.g. 'no new comments on PR #N, no competing PRs'",
8: "e.g. 'pushed to PR #N, notified reviewer @X'",
9: "e.g. 'extracted pattern: reviewer-priority-matrix'",
10: "e.g. 'no upstream gap found'",
11: "e.g. 'no plugin changes needed'",
12: "e.g. 'branch cleaned, PR status recorded'",
},
},
}
def _resolve(state):
"""Return the template definition dict for the current workflow."""
tname = state.get("template", "github-issue")
return TEMPLATES.get(tname, TEMPLATES["github-issue"])
def _load():
if not os.path.exists(STATE_FILE):
return None
try:
with open(STATE_FILE) as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
return None
def _save(state):
os.makedirs(STATE_DIR, exist_ok=True)
with open(STATE_FILE, "w") as f:
json.dump(state, f, indent=2, ensure_ascii=False)
def _now_iso():
return __import__("datetime").datetime.now().isoformat()
def cmd_start(template, issue=None):
existing = _load()
if existing is not None and existing.get("state") == "active":
print("BLOCKED: A workflow is already active. Complete or abort it first.")
sys.exit(1)
if template not in TEMPLATES:
print(f"Unknown template: {template}")
print(f"Available: {', '.join(TEMPLATES.keys())}")
sys.exit(1)
tpl = TEMPLATES[template]
state = {
"template": template,
"current_phase": 1,
"completed_phases": [],
"issue_number": issue,
"started_at": _now_iso(),
"last_advanced_at": None,
"state": "active",
"phase_evidence": {}, # {phase_number: {"evidence": str, "timestamp": str}}
}
_save(state)
print(f"✅ Started {template} workflow. Phase 1/{tpl['total']}: {tpl['PHASE_NAMES'][1]}")
print(f" State: {STATE_FILE}")
if template == "github-issue":
print("⛔ NO-COMMENT RULE: If a competing PR exists, do NOT comment/review it.")
print(" Only interact with PRs you (atyou2happy) authored. Complete early if competing.")
hints = tpl.get("EVIDENCE_HINTS", {})
print(f"\n 📝 Evidence needed to advance: {hints.get(1, 'describe what you did')}")
print(f" Usage: phase_tracker.py advance --evidence \"...\"")
def cmd_current():
state = _load()
if state is None:
print("No active workflow.")
return
tpl = _resolve(state)
cp = state["current_phase"]
done = state["completed_phases"]
names = tpl["PHASE_NAMES"]
print(f"📊 Workflow: {state['template']}")
print(f" Issue: {state.get('issue_number', 'N/A')}")
print(f" State: {state['state']}")
print(f" Current phase: {cp}/{tpl['total']}{names.get(cp, 'unknown')}")
print(f" Completed: {', '.join(str(p) for p in done) if done else 'none yet'}")
# Show evidence for completed phases
evidence = state.get("phase_evidence", {})
if evidence:
print(f"\n 📋 Evidence log:")
for p in sorted(done):
ev = evidence.get(str(p), {})
text = ev.get("evidence", "?")[:80]
print(f" Phase {p}: {text}{'...' if len(ev.get('evidence','')) > 80 else ''}")
if state["state"] == "active":
hints = tpl.get("EVIDENCE_HINTS", {})
print(f"\n⚠️ You are on Phase {cp}. Do NOT skip ahead!")
print(f" 📝 To advance: phase_tracker.py advance --evidence \"{hints.get(cp, 'describe what you did')}\"")
def cmd_check(expected_str):
state = _load()
if state is None:
print("BLOCKED: No active workflow. Call 'phase_tracker.py start <template>' first.")
sys.exit(1)
if state["state"] != "active":
print(f"BLOCKED: Workflow is {state['state']} (not active).")
sys.exit(1)
try:
expected = int(expected_str)
except ValueError:
print(f"BLOCKED: Invalid phase number: {expected_str}")
sys.exit(1)
tpl = _resolve(state)
names = tpl["PHASE_NAMES"]
current = state["current_phase"]
if current == expected:
print(f"✅ Phase {current}/{tpl['total']}{names.get(current, 'unknown')}")
hints = tpl.get("EVIDENCE_HINTS", {})
print(f" 📝 To advance: phase_tracker.py advance --evidence \"{hints.get(current, 'describe what you did')}\"")
sys.exit(0)
elif current < expected:
print(f"❌ BLOCKED: Cannot do Phase {expected} yet — you are on Phase {current} ({names.get(current, '')})")
print(f" Next expected: Phase {current}{names.get(current, '')}")
sys.exit(1)
else:
print(f"⚠️ Phase {expected} is already completed. You are on Phase {current}.")
print(f" Current: Phase {current}{names.get(current, '')}")
sys.exit(1)
def cmd_advance(evidence=None):
state = _load()
if state is None:
print("BLOCKED: No active workflow.")
sys.exit(1)
if state["state"] != "active":
print(f"BLOCKED: Workflow is {state['state']}.")
sys.exit(1)
tpl = _resolve(state)
names = tpl["PHASE_NAMES"]
total = tpl["total"]
current = state["current_phase"]
if current >= total:
print(f"⚠️ Already at final phase ({current}/{total}). Call 'complete' to finish.")
sys.exit(1)
# ── GUARD 1: Evidence required ──────────────────────────────
if not evidence or len(evidence.strip()) < 20:
hints = tpl.get("EVIDENCE_HINTS", {})
print(f"❌ BLOCKED: Evidence required to advance from Phase {current}.")
print(f" Phase {current}: {names.get(current, '')}")
print(f" Hint: {hints.get(current, 'describe what you did')}")
print(f" Usage: phase_tracker.py advance --evidence \"your evidence here (min 20 chars)\"")
sys.exit(1)
# ── GUARD 2: Phase-specific checks ──────────────────────────
# Phase 1 triage: if competing PR found, must acknowledge decision
if current == 1 and state.get("template") == "github-issue":
ev_lower = evidence.lower()
has_competitor = "compet" in ev_lower or "duplicate" in ev_lower or "already" in ev_lower
if has_competitor and "no-comment" not in ev_lower and "did not comment" not in ev_lower:
print("⚠️ WARNING: Competing PR detected but no explicit no-comment declaration.")
print(" Add 'no-comment' or 'did not comment' to evidence if you found a competitor.")
print(" ⛔ REMINDER: NEVER comment on PRs you did not author.")
# Record evidence
if "phase_evidence" not in state:
state["phase_evidence"] = {}
state["phase_evidence"][str(current)] = {
"evidence": evidence.strip(),
"timestamp": _now_iso(),
}
if current not in state["completed_phases"]:
state["completed_phases"].append(current)
next_phase = current + 1
state["current_phase"] = next_phase
state["last_advanced_at"] = _now_iso()
state["last_updated"] = _now_iso()
_save(state)
print(f"✅ Advanced to Phase {next_phase}/{total}: {names.get(next_phase, 'unknown')}")
print(f" Evidence recorded: {evidence.strip()[:100]}{'...' if len(evidence.strip()) > 100 else ''}")
hints = tpl.get("EVIDENCE_HINTS", {})
if next_phase <= total:
print(f"\n Next: Phase {next_phase}{names.get(next_phase, '')}")
print(f" 📝 Evidence needed: {hints.get(next_phase, 'describe what you did')}")
print(f" Usage: phase_tracker.py advance --evidence \"...\"")
if next_phase == total:
print(f" 🏁 Final phase! Complete with: phase_tracker.py complete")
def cmd_skip(target_str):
state = _load()
if state is None:
print("BLOCKED: No active workflow.")
sys.exit(1)
try:
target = int(target_str)
except ValueError:
print(f"Invalid phase: {target_str}")
sys.exit(1)
current = state["current_phase"]
tpl = _resolve(state)
names = tpl["PHASE_NAMES"]
print(f"❌ BLOCKED: Skipping disabled. Phase {current}{target}")
print(f" Current: Phase {current}{names.get(current, '')}")
print(f" Use 'advance --evidence \"...\"' to advance properly.")
sys.exit(1)
def cmd_complete():
state = _load()
if state is None:
print("No active workflow.")
return
tpl = _resolve(state)
total = tpl["total"]
cp = state["current_phase"]
done = state["completed_phases"]
# Auto-mark the final phase as completed (advance blocks on the last phase
# because there's nowhere to advance to — complete is the only way out).
if cp == total and cp not in done:
done.append(cp)
# Check if all phases were completed
missing = [p for p in range(1, total + 1) if p not in done]
if missing:
print(f"⚠️ WARNING: Phases not completed: {missing}")
print(f" These phases were skipped without evidence.")
evidence = state.get("phase_evidence", {})
logged = sorted(evidence.keys())
print(f" Evidence logged for phases: {logged if logged else 'none'}")
print(f" Call 'complete --force' to override, or go back and do the work.")
# Check for --force flag
if "--force" not in sys.argv:
sys.exit(1)
print(" (--force used, proceeding anyway)")
state["state"] = "completed"
if state["current_phase"] not in state["completed_phases"]:
state["completed_phases"].append(state["current_phase"])
state["last_updated"] = _now_iso()
_save(state)
print(f"✅ Workflow '{state['template']}' COMPLETED.")
print(f" Phases completed: {len(done)}/{total}")
def cmd_abort():
state = _load()
if state is None:
print("No active workflow.")
return
state["state"] = "aborted"
state["last_updated"] = _now_iso()
_save(state)
tpl = _resolve(state)
evidence = state.get("phase_evidence", {})
print(f"⛔ Workflow '{state['template']}' ABORTED at Phase {state['current_phase']}.")
if evidence:
print(f" Evidence recorded for: {sorted(evidence.keys())}")
def cmd_list_phases():
for tname, tpl in TEMPLATES.items():
names = tpl["PHASE_NAMES"]
hints = tpl.get("EVIDENCE_HINTS", {})
print(f"\n📋 {tname} ({tpl['total']} phases)")
print("=" * 60)
for i in range(1, tpl["total"] + 1):
print(f" Phase {i:2d}: {names.get(i, '')}")
if hints.get(i):
print(f" Evidence: {hints[i]}")
print()
def main():
if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
print("Usage:")
print(" phase_tracker.py start <template> [--issue N] — start new workflow")
print(" phase_tracker.py current — show current phase")
print(" phase_tracker.py check <N> — verify on expected phase")
print(" phase_tracker.py advance --evidence \"text\" — submit evidence + advance")
print(" phase_tracker.py skip <N> — BLOCKED")
print(" phase_tracker.py complete [--force] — mark workflow done")
print(" phase_tracker.py abort — abort workflow")
print(" phase_tracker.py phases — list templates + phases")
print(f"\nTemplates: {', '.join(TEMPLATES.keys())}")
return
cmd = sys.argv[1]
if cmd == "start":
template = sys.argv[2] if len(sys.argv) > 2 else "github-issue"
issue = None
if "--issue" in sys.argv:
idx = sys.argv.index("--issue")
if idx + 1 < len(sys.argv):
issue = int(sys.argv[idx + 1])
cmd_start(template, issue)
elif cmd == "current":
cmd_current()
elif cmd == "check":
if len(sys.argv) < 3:
print("Usage: phase_tracker.py check <phase_number>")
sys.exit(1)
cmd_check(sys.argv[2])
elif cmd == "advance":
evidence = None
if "--evidence" in sys.argv:
idx = sys.argv.index("--evidence")
if idx + 1 < len(sys.argv):
evidence = sys.argv[idx + 1]
cmd_advance(evidence)
elif cmd == "skip":
if len(sys.argv) < 3:
print("Usage: phase_tracker.py skip <phase_number>")
sys.exit(1)
cmd_skip(sys.argv[2])
elif cmd == "complete":
cmd_complete()
elif cmd == "abort":
cmd_abort()
elif cmd == "phases":
cmd_list_phases()
else:
print(f"Unknown command: {cmd}")
sys.exit(1)
if __name__ == "__main__":
main()

76
scripts/phase_watchdog.sh Executable file
View file

@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
# Phase Tracker Watchdog
# Runs via cron to detect stalled upstream contribution workflows.
# If a workflow hasn't been updated in STALL_HOURS hours, alerts the user.
HERMES_AGENT_HOME="${HOME}/.hermes/hermes-agent"
TRACKER="$HERMES_AGENT_HOME/.dev-workflow/phase-tracker.json"
STALL_HOURS=4
if [ ! -f "$TRACKER" ]; then
exit 0 # No active workflow
fi
STATE=$(python3 -c "
import json
with open('$TRACKER') as f:
d = json.load(f)
print(json.dumps({
'state': d.get('state', 'unknown'),
'phase': d.get('current_phase', 0),
'issue': d.get('issue_number', 'N/A'),
'updated': d.get('last_updated', ''),
'started': d.get('started_at', ''),
}))
" 2>/dev/null || echo '{}')
STATE_VAL=$(echo "$STATE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('state',''))" 2>/dev/null || echo "")
PHASE=$(echo "$STATE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('phase',0))" 2>/dev/null || echo "0")
ISSUE=$(echo "$STATE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('issue','N/A'))" 2>/dev/null || echo "N/A")
UPDATED=$(echo "$STATE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('updated',''))" 2>/dev/null || echo "")
STARTED=$(echo "$STATE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('started',''))" 2>/dev/null || echo "")
if [ "$STATE_VAL" != "active" ]; then
exit 0 # Completed or aborted
fi
# Check if stalled
if [ -n "$UPDATED" ]; then
NOW=$(date +%s)
UPDATED_TS=$(date -d "$UPDATED" +%s 2>/dev/null || echo "0")
if [ "$UPDATED_TS" -gt 0 ]; then
ELAPSED=$(( (NOW - UPDATED_TS) / 3600 ))
if [ "$ELAPSED" -ge "$STALL_HOURS" ]; then
PHASE_NAME=$(python3 -c "
PHASE_NAMES = {
1: 'Deep Triage', 2: 'Branch Setup', 3: 'Code Understanding',
4: 'Security Baseline', 5: 'Implementation', 6: 'Test Suite',
7: 'Code Review', 8: 'PR Readiness Check', 9: 'Create Pull Request',
10: 'Self Evolution', 11: 'Insight Feature Request', 12: 'Plugin Improve',
13: 'Cleanup',
}
print(PHASE_NAMES.get($PHASE, 'unknown'))
" 2>/dev/null || echo "unknown")
cat <<WATCHDOG
⚠️ Phase Tracker STALLED
Workflow for issue #${ISSUE} has been stuck at Phase ${PHASE} (${PHASE_NAME}) for ${ELAPSED} hours.
To continue:
cd $HERMES_HOME/hermes-agent
python3 scripts/phase_tracker.py current # check current phase
python3 scripts/phase_tracker.py advance # advance to next phase
Or to abort:
python3 scripts/phase_tracker.py abort
Last updated: ${UPDATED}
WATCHDOG
fi
fi
fi
exit 0

View file

@ -0,0 +1,74 @@
"""Tests for Kimi/Moonshot prompt cache inclusion (issue #25970)."""
import pytest
import sys
import os
from unittest.mock import MagicMock, patch
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def _call_cache_policy(model: str, base_url: str, provider: str = "",
api_mode: str = "chat_completions"):
"""Call _anthropic_prompt_cache_policy without full AIAgent init."""
# Import the standalone helper used by the policy
from run_agent import AIAgent
# Create a shell object to call the method on
agent = object.__new__(AIAgent)
# Set only the attributes the method reads
agent.provider = provider
agent.base_url = base_url
agent.api_mode = api_mode
agent.model = model
return agent._anthropic_prompt_cache_policy()
class TestKimiPrefixCache:
def test_kimi_k26_openrouter(self):
result = _call_cache_policy(
model="moonshotai/kimi-k2.6",
base_url="https://openrouter.ai/api/v1",
)
assert result == (True, False)
def test_kimi_k2_openrouter(self):
result = _call_cache_policy(
model="kimi-k2",
base_url="https://openrouter.ai/api/v1",
)
assert result == (True, False)
def test_moonshot_v1_openrouter(self):
result = _call_cache_policy(
model="moonshotai/moonshot-v1-8k",
base_url="https://openrouter.ai/api/v1",
)
assert result == (True, False)
def test_kimi_non_openrouter_no_caching(self):
result = _call_cache_policy(
model="moonshotai/kimi-k2.6",
base_url="https://api.example.com/v1",
)
assert result == (False, False)
def test_claude_openrouter_regression(self):
result = _call_cache_policy(
model="anthropic/claude-sonnet-4",
base_url="https://openrouter.ai/api/v1",
)
assert result == (True, False)
def test_kimi_nous_portal(self):
result = _call_cache_policy(
model="moonshotai/kimi-k2.6",
base_url="https://nousresearch.com/api/v1",
)
assert result == (True, False)
def test_random_model_no_caching(self):
result = _call_cache_policy(
model="some-random-model",
base_url="https://api.example.com/v1",
)
assert result == (False, False)