"""Closed worker lifecycle handlers, called only inside the receipt transaction.""" import json import time from hermes_state_runtime import RuntimeStoreError _IDENTITY_FIELDS = frozenset({ 'user_id', 'session_key', 'chat_id', 'chat_type', 'thread_id', 'parent_session_id', 'cwd', 'profile_name', 'git_repo_root', 'origin_json', 'display_name', }) _CREATE_FIELDS = _IDENTITY_FIELDS | {'source', 'model', 'model_config', 'system_prompt'} _CONSTRUCTOR_CONFIG = frozenset({'max_iterations', 'max_tokens', 'reasoning_config', 'yolo_mode'}) def worker_create(db, conn, session_id, payload): if set(payload) - _CREATE_FIELDS or 'source' not in payload: raise RuntimeStoreError('invalid_params') row = conn.execute('SELECT * FROM sessions WHERE id=?', (session_id,)).fetchone() # A worker cannot mint a row or supply unreserved routing, lineage or cwd. if row is None or payload['source'] != row['source']: raise RuntimeStoreError('permission_denied') for key in _IDENTITY_FIELDS: value = payload.get(key) if value is not None and value != row[key]: raise RuntimeStoreError('permission_denied') for key in _CREATE_FIELDS - {'model_config'}: if payload.get(key) is not None and not isinstance(payload[key], str): raise RuntimeStoreError('invalid_params') config = payload.get('model_config') if config is not None and (not isinstance(config, dict) or set(config) - _CONSTRUCTOR_CONFIG): raise RuntimeStoreError('invalid_params') insert_session_row_in_transaction(db, conn, session_id, **payload) if config: # An owner-reserved child row already holds its lineage marker, so the keep-existing upsert # would drop these facts; merge them beside it (keys are the constructor set only). conn.execute("UPDATE sessions SET model_config=json_patch(model_config,?) WHERE id=? " "AND json_type(model_config,'$._delegate_from') IS NOT NULL", (json.dumps(config), session_id)) return {'value': session_id} def worker_end(db, conn, session_id, payload): if set(payload) != {'end_reason'} or not isinstance(payload['end_reason'], str) or not payload['end_reason']: raise RuntimeStoreError('invalid_params') db._end_and_bump(conn, 'UPDATE sessions SET ended_at=?,end_reason=? WHERE id=? AND ended_at IS NULL', (time.time(), payload['end_reason'], session_id), session_id, payload['end_reason']) return {'value': None} def worker_lifecycle(db, conn, session_id, payload): from hermes_state_sessions import classify_session_status if payload: raise RuntimeStoreError('invalid_params') row = conn.execute("SELECT role,tool_calls IS NOT NULL AS has_tool_calls,finish_reason " "FROM messages WHERE session_id=? ORDER BY id DESC LIMIT 1", (session_id,)).fetchone() value = classify_session_status(role=row['role'], has_tool_calls=bool(row['has_tool_calls']), finish_reason=row['finish_reason']) if row else 'empty' return {'value': value} def worker_title(db, conn, session_id, payload): if (set(payload) != {'title', 'source'} or payload['source'] not in ('user', 'derived', 'llm') or not (payload['title'] is None or isinstance(payload['title'], str))): raise RuntimeStoreError('invalid_params') try: value = bool(db._set_session_title_in_transaction( conn, session_id, payload['title'], source=payload['source'])) except ValueError: # Expected validation/conflict outcomes must not poison the retry journal. # Keep candidate authority in the receipt, not an in-memory worker claim. return {'value': False, 'error': 'Title unavailable or invalid', 'title_candidate': payload['title']} return {'value': value} def worker_title_source(db, conn, session_id, payload): if set(payload) != {'source'} or payload['source'] not in ('user', 'derived', 'llm'): raise RuntimeStoreError('invalid_params') return {'value': conn.execute( 'UPDATE sessions SET title_source=? WHERE id=? AND title IS NOT NULL', (payload['source'], session_id)).rowcount > 0} def worker_next_title(db, conn, session_id, payload): from hermes_state_titles import _NUMBERED_TITLE_RE from hermes_state_common import escape_like if set(payload) != {'base_title'} or not isinstance(payload['base_title'], str): raise RuntimeStoreError('invalid_params') title = conn.execute('SELECT title FROM sessions WHERE id=?', (session_id,)).fetchone()[0] # Only an assigned title or a receipted title attempt authorizes dedupe. # Concurrent activity/usage receipts must not invalidate that title attempt. candidate = payload['base_title'] attempted = conn.execute( "SELECT 1 FROM worker_receipts r JOIN worker_executions w ON w.execution_id=r.execution_id " "WHERE w.session_id=? AND w.status='running' " "AND json_extract(r.result_json,'$.title_candidate')=? LIMIT 1", (session_id, candidate)).fetchone() if candidate != title and attempted is None: raise RuntimeStoreError('permission_denied') match = _NUMBERED_TITLE_RE.match(candidate) base = match.group(1) if match else candidate rows = conn.execute("SELECT title FROM sessions WHERE title=? OR title LIKE ? ESCAPE '\\'", (base, f'{escape_like(base)} #%')).fetchall() if not rows: return {'value': base} numbers = [int(m.group(2)) for m in (_NUMBERED_TITLE_RE.match(row['title']) for row in rows) if m] return {'value': f'{base} #{max([1, *numbers]) + 1}'} def worker_activity(db, conn, session_id, payload): import math from agent.session_activity import bound_activity_description, normalize_activity_provenance if (set(payload) != {'ts', 'description', 'provenance'} or any(payload[k] is not None and not isinstance(payload[k], str) for k in ('description', 'provenance'))): raise RuntimeStoreError('invalid_params') when = payload['ts'] if when is not None and (type(when) not in (float, int) or not math.isfinite(when)): raise RuntimeStoreError('invalid_params') when = time.time() if when is None else float(when) conn.execute('UPDATE sessions SET last_activity_at=?,last_activity_description=?,last_activity_provenance=? ' 'WHERE id=? AND (last_activity_at IS NULL OR last_activity_at