hermes: group legacy triage API runs

This commit is contained in:
jenkins 2026-08-13 01:14:31 -03:00
parent 33df446a68
commit 42d4b34557
4 changed files with 40 additions and 15 deletions

View File

@ -224,6 +224,8 @@ spec:
/opt/hermes/.venv/bin/python /opt/coordinator/migrate_api_session_lineage.py /opt/hermes/.venv/bin/python /opt/coordinator/migrate_api_session_lineage.py
env: env:
- {name: HERMES_API_DEFAULT_PARENT_SESSION_ID, value: automated-triage} - {name: HERMES_API_DEFAULT_PARENT_SESSION_ID, value: automated-triage}
- name: HERMES_API_DEFAULT_PARENT_MATCH_PREFIXES
value: "A static-analysis finding, not a build failure.||Use $triage-titan-test-failures."
securityContext: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
runAsUser: 10000 runAsUser: 10000
@ -309,8 +311,8 @@ spec:
value: triage value: triage
- name: HERMES_API_DEFAULT_PARENT_SESSION_ID - name: HERMES_API_DEFAULT_PARENT_SESSION_ID
value: automated-triage value: automated-triage
- name: HERMES_API_DEFAULT_PARENT_MATCH_PREFIX - name: HERMES_API_DEFAULT_PARENT_MATCH_PREFIXES
value: A static-analysis finding, not a build failure. value: "A static-analysis finding, not a build failure.||Use $triage-titan-test-failures."
# Claude subscription OAuth token (sk-ant-oat01...). The anthropic # Claude subscription OAuth token (sk-ant-oat01...). The anthropic
# provider accepts ANTHROPIC_API_KEY, ANTHROPIC_TOKEN, or this, in # provider accepts ANTHROPIC_API_KEY, ANTHROPIC_TOKEN, or this, in
# that order; an OAuth token is not an API key, so it must arrive # that order; an OAuth token is not an API key, so it must arrive

View File

@ -27,12 +27,15 @@ LEGACY_ORPHANED_SMOKE_SESSIONS = {
} }
TRIAGE_PARENT = "automated-triage" TRIAGE_PARENT = "automated-triage"
TRIAGE_PARENT_TITLE = "Automated triage" TRIAGE_PARENT_TITLE = "Automated triage"
TRIAGE_MESSAGE_PREFIX = "A static-analysis finding, not a build failure." TRIAGE_MESSAGE_PREFIXES = (
"A static-analysis finding, not a build failure.",
"Use $triage-titan-test-failures.",
)
def _triage_title(message: str, session_id: str) -> str: def _triage_title(message: str, session_id: str) -> str:
"""Create a concise label from Ariadne's stable incident contract.""" """Create a concise label from Ariadne's stable incident contract."""
match = re.search(r"for incident ([^\s]+)\.", message) match = re.search(r"(?:for|Analyze) incident ([^\s.]+)(?:\.|\s)", message)
if not match: if not match:
return f"Automated triage run · {session_id[-8:]}" return f"Automated triage run · {session_id[-8:]}"
parts = match.group(1).split("/") parts = match.group(1).split("/")
@ -40,6 +43,11 @@ def _triage_title(message: str, session_id: str) -> str:
return f"Sonar · {label} · {session_id[-8:]}" return f"Sonar · {label} · {session_id[-8:]}"
def _is_automated_triage(message: str) -> bool:
"""Recognize only the stable Ariadne automation contracts."""
return message.startswith(TRIAGE_MESSAGE_PREFIXES)
def migrate_triage_group(connection: sqlite3.Connection) -> int: def migrate_triage_group(connection: sqlite3.Connection) -> int:
"""Nest only recognized automated Sonar runs under one durable parent.""" """Nest only recognized automated Sonar runs under one durable parent."""
rows = connection.execute( rows = connection.execute(
@ -51,11 +59,10 @@ def migrate_triage_group(connection: sqlite3.Connection) -> int:
AND s.parent_session_id IS NULL AND s.parent_session_id IS NULL
AND s.archived = 0 AND s.archived = 0
AND m.role = 'user' AND m.role = 'user'
AND m.content LIKE ?
ORDER BY s.started_at ORDER BY s.started_at
""", """
(f"{TRIAGE_MESSAGE_PREFIX}%",),
).fetchall() ).fetchall()
rows = [row for row in rows if _is_automated_triage(str(row[2] or ""))]
if not rows: if not rows:
return 0 return 0
started_at = min(float(row[1] or time.time()) for row in rows) started_at = min(float(row[1] or time.time()) for row in rows)

View File

@ -65,8 +65,12 @@ RUNS_AFTER = ''' run_id = f"run_{uuid.uuid4().hex}"
"X-Hermes-Parent-Session-Id" "X-Hermes-Parent-Session-Id"
) )
default_parent = os.environ.get("HERMES_API_DEFAULT_PARENT_SESSION_ID", "").strip() default_parent = os.environ.get("HERMES_API_DEFAULT_PARENT_SESSION_ID", "").strip()
default_prefix = os.environ.get("HERMES_API_DEFAULT_PARENT_MATCH_PREFIX", "").strip() default_prefixes = tuple(
if not raw_parent and default_parent and default_prefix and user_message.startswith(default_prefix): item.strip()
for item in os.environ.get("HERMES_API_DEFAULT_PARENT_MATCH_PREFIXES", "").split("||")
if item.strip()
)
if not raw_parent and default_parent and default_prefixes and user_message.startswith(default_prefixes):
raw_parent = default_parent raw_parent = default_parent
parent_session_id = str(raw_parent).strip() if raw_parent else None parent_session_id = str(raw_parent).strip() if raw_parent else None
@ -92,7 +96,7 @@ RUNS_AFTER = ''' run_id = f"run_{uuid.uuid4().hex}"
system_prompt=instructions if isinstance(instructions, str) else None, system_prompt=instructions if isinstance(instructions, str) else None,
parent_session_id=parent_session_id, parent_session_id=parent_session_id,
) )
incident = re.search(r"for incident ([^\\s]+)\\.", user_message) incident = re.search(r"(?:for|Analyze) incident ([^\\s.]+)(?:\\.|\\s)", user_message)
if incident and parent_session_id == default_parent: if incident and parent_session_id == default_parent:
parts = incident.group(1).split("/") parts = incident.group(1).split("/")
label = " · ".join(parts[1:3]) if len(parts) >= 3 else incident.group(1) label = " · ".join(parts[1:3]) if len(parts) >= 3 else incident.group(1)

View File

@ -997,8 +997,8 @@ def test_api_session_patch_accepts_parent_lineage(tmp_path: Path):
assert "X-Hermes-Parent-Session-Id" in patched assert "X-Hermes-Parent-Session-Id" in patched
assert "parent_session_id=parent_session_id" in patched assert "parent_session_id=parent_session_id" in patched
assert "Parent session not found" in patched assert "Parent session not found" in patched
assert "HERMES_API_DEFAULT_PARENT_MATCH_PREFIX" in patched assert "HERMES_API_DEFAULT_PARENT_MATCH_PREFIXES" in patched
assert "user_message.startswith(default_prefix)" in patched assert "user_message.startswith(default_prefixes)" in patched
def test_legacy_api_sessions_are_nested_idempotently(tmp_path: Path): def test_legacy_api_sessions_are_nested_idempotently(tmp_path: Path):
@ -1074,21 +1074,26 @@ def test_automated_triage_sessions_are_grouped_without_touching_interactive_runs
) )
connection.executemany( connection.executemany(
"INSERT INTO sessions (id, source, started_at) VALUES (?, 'api_server', ?)", "INSERT INTO sessions (id, source, started_at) VALUES (?, 'api_server', ?)",
(("triage-run", 1.0), ("interactive-run", 2.0)), (("triage-run", 1.0), ("jenkins-run", 1.5), ("interactive-run", 2.0)),
) )
connection.executemany( connection.executemany(
"INSERT INTO messages (session_id, role, content) VALUES (?, 'user', ?)", "INSERT INTO messages (session_id, role, content) VALUES (?, 'user', ?)",
( (
( (
"triage-run", "triage-run",
module.TRIAGE_MESSAGE_PREFIX module.TRIAGE_MESSAGE_PREFIXES[0]
+ " Fix for incident sonar/bstein_home/python:S2208/finding-key.", + " Fix for incident sonar/bstein_home/python:S2208/finding-key.",
), ),
(
"jenkins-run",
module.TRIAGE_MESSAGE_PREFIXES[1]
+ "\nAnalyze incident soteria/291 for the Jenkins job soteria.",
),
("interactive-run", "Please explain this alert to me."), ("interactive-run", "Please explain this alert to me."),
), ),
) )
assert module.migrate(database, group_triage=True) == 1 assert module.migrate(database, group_triage=True) == 2
assert module.migrate(database, group_triage=True) == 0 assert module.migrate(database, group_triage=True) == 0
with sqlite3.connect(database) as connection: with sqlite3.connect(database) as connection:
parent = connection.execute( parent = connection.execute(
@ -1100,12 +1105,19 @@ def test_automated_triage_sessions_are_grouped_without_touching_interactive_runs
interactive = connection.execute( interactive = connection.execute(
"SELECT parent_session_id FROM sessions WHERE id = 'interactive-run'" "SELECT parent_session_id FROM sessions WHERE id = 'interactive-run'"
).fetchone() ).fetchone()
jenkins = connection.execute(
"SELECT parent_session_id, title FROM sessions WHERE id = 'jenkins-run'"
).fetchone()
assert parent == (module.TRIAGE_PARENT_TITLE,) assert parent == (module.TRIAGE_PARENT_TITLE,)
assert triage == ( assert triage == (
module.TRIAGE_PARENT, module.TRIAGE_PARENT,
"Sonar · bstein_home · python:S2208 · iage-run", "Sonar · bstein_home · python:S2208 · iage-run",
) )
assert interactive == (None,) assert interactive == (None,)
assert jenkins == (
module.TRIAGE_PARENT,
"Sonar · soteria/291 · kins-run",
)
def test_switchyard_brokers_and_native_claude_lane_use_the_right_images(): def test_switchyard_brokers_and_native_claude_lane_use_the_right_images():