feat(t3code): thread events as master injects, hook job without dedupe

This commit is contained in:
hh
2026-08-29 22:48:45 +02:00
parent 1da5b36cca
commit 002230137e
3 changed files with 56 additions and 0 deletions
+2
View File
@@ -70,6 +70,8 @@ BEAVERGRAM_MCP=
# На каждой машине: `t3 auth session issue --token-only --ttl 365d --label beaver` # На каждой машине: `t3 auth session issue --token-only --ttl 365d --label beaver`
T3_MAC_TOKEN= T3_MAC_TOKEN=
T3_DELL_TOKEN= T3_DELL_TOKEN=
# Токен gateway со scope `api` (admin → Tokens) - им t3code-mcp шлёт события в /hooks/t3code
T3CODE_MCP_GATEWAY_TOKEN=
# Ветка/тег t3code-mcp, из которого собирается образ # Ветка/тег t3code-mcp, из которого собирается образ
T3CODE_MCP_REF=main T3CODE_MCP_REF=main
+48
View File
@@ -2,6 +2,7 @@ import logging
import os import os
from datetime import UTC, date, datetime, timedelta from datetime import UTC, date, datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from beaver_gateway.agents.base import BaseAgent, ExposedMcp from beaver_gateway.agents.base import BaseAgent, ExposedMcp
@@ -589,6 +590,51 @@ async def memory(run: JobRun) -> None:
) )
T3CODE_QUESTION_HINT = (
"Если ответ следует из задания - ответь сам: t3_answer(thread_id, "
"{id вопроса: label}). Не знаешь - спроси Бобра через say своими словами, "
"потом t3_answer. Тред стоит, пока не ответят."
)
def t3code_text(p: dict[str, Any]) -> str:
head = (
f"t3code: тред «{p.get('title')}» ({p.get('machine')}, {p.get('project')}, "
f"thread_id {p.get('thread_id')})"
)
event = p.get("event")
if event in ("question", "approval"):
lines = [f"{head} - спрашивает."]
for q in (p.get("question") or {}).get("questions", []):
multi = " (можно несколько)" if q.get("multi_select") else ""
lines.append(
f"- [{q['id']}] {q.get('header') or ''}: {q['question']}{multi}"
)
lines.extend(
f" · {o['label']}"
+ (f" - {o['description']}" if o.get("description") else "")
for o in q.get("options", [])
)
lines.append(T3CODE_QUESTION_HINT)
return "\n".join(lines)
if event == "completed":
lines = [f"{head} - завершён."]
elif event == "interrupted":
lines = [f"{head} - прерван."]
else:
lines = [f"{head} - упал: {p.get('error') or 'без текста ошибки'}."]
if p.get("text"):
lines.append(f"Ответ треда:\n{p['text']}")
if p.get("files"):
lines.append("Файлы: " + ", ".join(f["path"] for f in p["files"]))
return "\n".join(lines)
async def t3code_event(run: JobRun) -> None:
"""§5, §8.6: t3code-mcp следит за тредами по WS и присылает сюда события."""
await run.inject_master(t3code_text(run.payload), urgency="urgent", origin="t3code")
async def komodo_alert(run: JobRun) -> None: async def komodo_alert(run: JobRun) -> None:
payload = run.payload payload = run.payload
summary = payload.get("raw") or {k: v for k, v in payload.items() if k != "trigger"} summary = payload.get("raw") or {k: v for k, v in payload.items() if k != "trigger"}
@@ -602,6 +648,8 @@ jobs = [
Job("память", memory, cron="30 4 * * 0", critical=False), Job("память", memory, cron="30 4 * * 0", critical=False),
Job("deploy", deploy, webhook=True), Job("deploy", deploy, webhook=True),
Job("komodo", komodo_alert, webhook=True), Job("komodo", komodo_alert, webhook=True),
# события идут подряд (вопрос и завершение) - схлопывать нельзя
Job("t3code", t3code_event, webhook=True, dedupe=False),
] ]
gateway = Gateway( gateway = Gateway(
+6
View File
@@ -97,10 +97,16 @@ services:
profiles: [t3] profiles: [t3]
<<: *restart <<: *restart
env_file: .env env_file: .env
environment:
# события тредов (завершён / упал / спрашивает) → инжект в мастер (§4.5)
T3CODE_MCP_HOOK_URL: http://gateway:62990/hooks/t3code
T3CODE_MCP_STATE: /data/t3code-mcp.json
volumes: volumes:
- ./t3code.toml:/config/t3code.toml:ro - ./t3code.toml:/config/t3code.toml:ro
- t3code-state:/data
volumes: volumes:
t3code-state:
postgres-data: postgres-data:
vault: vault:
obsidian-config: obsidian-config: