Design: Subagent RAM = 0 — visibility bez blokowania kolejki

Status: approved — implements migration 039 Related issue: see GitHub issue created from this doc Implemented by: migration monitoring/supabase/migrations/039_subagent_zero_ram.sql


Problem

Subagenty spawnowane przez Agent tool działają w chmurze Anthropic, nie na bms-4. Obecna wartość "subagent":1 w weight_ram_gb (z migration 038) powoduje, że każdy subagent rezerwuje 1 GB RAM bms-4 w available_ram_gb() — czego w rzeczywistości nie zużywa. Przy dużej liczbie subagentów blokuje to dispatchera przed uruchamianiem kolejnych workerów.

Cel: subagenty nadal rejestrowane w kolejce (visibility: ile równolegle działa na licencji Anthropic), ale ich RAM = 0 — nie wpływają na budżet RAM bms-4.


Decyzje architektoniczne

PytanieDecyzjaUzasadnienie
Czy subagenty liczyć do emergency_max_workers?NIEemergency_max_workers chroni zasoby hosta; cloud subagents zużywają 0 zasobów hosta
Czy rejestrować subagenty w kolejce?TAKwidoczność: ile równolegle działa na licencji Anthropic
Limit dla v_can_fit gdy slot_ram=0?p_count bez ograniczeńbrak RAM-gate = brak powodu do limitu; licznik Grafana jest informacyjny
emergency_max_workers count w else-branchy?wyklucz job_type=‘subagent’subagent-rows nie powinny zmniejszać dostępnych slotów dla prawdziwych workerów

Zmiana 1 — Migracja 039_subagent_zero_ram.sql

UPDATE weight_ram_gb (subagent: 1 → 0)

UPDATE dev_r_server_capacity SET
  weight_ram_gb = '{"light":3,"heavy":6,"playwright":6,"subagent":0,"orchestrator":12}'
WHERE server_label = 'bms-4';
 
UPDATE dev_r_server_capacity SET
  weight_ram_gb = '{"light":3,"heavy":5,"playwright":5,"subagent":0,"orchestrator":12}'
WHERE server_label = 'vps-i1';

available_ram_gb() — bez zmian. Gdy weight='subagent' i wartość=0, SUM dodaje 0. Matematycznie poprawne.

Fix reserve_subagent_slots() — obsługa v_slot_ram=0

CREATE OR REPLACE FUNCTION public.reserve_subagent_slots(
  p_parent_session_id TEXT,
  p_weight            TEXT,
  p_count             INT,
  p_server_label      TEXT,
  p_parent_issue      INT DEFAULT NULL
) RETURNS TABLE(slot_id BIGINT) LANGUAGE plpgsql VOLATILE SECURITY DEFINER AS $$
DECLARE
  v_avail_ram   NUMERIC;
  v_slot_ram    NUMERIC;
  v_capacity    dev_r_server_capacity%ROWTYPE;
  v_can_fit     INT;
BEGIN
  PERFORM pg_advisory_xact_lock(hashtext(p_server_label));
  SELECT * INTO v_capacity FROM dev_r_server_capacity WHERE server_label = p_server_label;
  v_slot_ram  := (v_capacity.weight_ram_gb ->> p_weight)::NUMERIC;
  v_avail_ram := available_ram_gb(p_server_label);
 
  IF v_slot_ram = 0 THEN
    -- Cloud subagents: zero host resources — no RAM gate, no emergency_max cap
    v_can_fit := p_count;
  ELSE
    v_can_fit := LEAST(
      p_count,
      GREATEST(0, floor(v_avail_ram / v_slot_ram)::INT),
      GREATEST(0, v_capacity.emergency_max_workers - (
        -- Exclude cloud subagents (job_type='subagent') — they don't consume host resources
        SELECT COUNT(*) FROM dev_r_worker_queue
        WHERE server_node = p_server_label
          AND status IN ('claimed','running')
          AND job_type != 'subagent'
      ))
    );
  END IF;
 
  IF v_can_fit = 0 THEN RETURN; END IF;
 
  RETURN QUERY
    INSERT INTO dev_r_worker_queue
      (job_type, weight, status, server_node, parent_session_id, parent_issue_number,
       priority, queued_at, claimed_at, started_at)
    SELECT
      'subagent', p_weight, 'running', p_server_label, p_parent_session_id, p_parent_issue,
      50, NOW(), NOW(), NOW()
    FROM generate_series(1, v_can_fit)
    RETURNING id;
END;
$$;

Fix max_subagents_for_weight() — CASE gdy dzielnik=0

-- p_weight ignored when subagent weight_ram_gb=0; returns emergency_max_workers as concurrency cap
CREATE OR REPLACE FUNCTION public.max_subagents_for_weight(p_server_label TEXT, p_weight TEXT)
RETURNS INT LANGUAGE sql STABLE SECURITY DEFINER AS $$
  SELECT CASE
    WHEN (s.weight_ram_gb ->> 'subagent')::NUMERIC = 0
      THEN s.emergency_max_workers
    ELSE GREATEST(0, floor(
      ((s.os_ram_gb - s.reserved_ram_gb) * 0.80
        - (s.weight_ram_gb ->> p_weight)::NUMERIC)
      / (s.weight_ram_gb ->> 'subagent')::NUMERIC
    )::INT)
  END
  FROM dev_r_server_capacity s
  WHERE s.server_label = p_server_label
$$;

DOWN block

-- DOWN (przywraca stan migration 038):
-- UPDATE dev_r_server_capacity SET
--   weight_ram_gb = '{"light":3,"heavy":6,"playwright":6,"subagent":1,"orchestrator":12}'
-- WHERE server_label = 'bms-4';
-- UPDATE dev_r_server_capacity SET
--   weight_ram_gb = '{"light":3,"heavy":5,"playwright":5,"subagent":1,"orchestrator":12}'
-- WHERE server_label = 'vps-i1';
-- (restore reserve_subagent_slots and max_subagents_for_weight to migration 038 versions)

Zmiana 2 — worker-issue.md Step 0b

Usunąć:

  • blok exit-4 gdy slots.Count < PLANNED_SUBAGENTS
  • ghost-slot cleanup (DELETE przed exit-4)

Zostaje:

  • max_subagents_for_weight() jako soft cap advisory (nie blokada)
  • reserve_subagent_slots() dla rejestracji wierszy widoczności
  • $SLOT_IDS przekazywane do Step Final dla complete_subagent_slot

Pseudokod po zmianie:

$softCap = max_subagents_for_weight($SERVER_LABEL, $WEIGHT)   # 12 gdy subagent=0
$PLANNED = [Math]::Min($PLANNED_SUBAGENTS, $softCap)
$slots   = reserve_subagent_slots(SESSION_ID, 'subagent', $PLANNED, $SERVER_LABEL, $ISSUE_NUMBER)
$SLOT_IDS = $slots | ForEach-Object { $_.slot_id }
# spawn $slots.Count agentów — brak exit-4

Zmiana 3 — scripts/queue-dispatcher-loop.py

Wyłącznie komunikat exit-4 (~L392):

# Było:
"error_message": f"Re-queued as {next_weight} (needs subagents)"
# Jest:
"error_message": f"Re-queued as {next_weight} (worker self-assessment: needs heavier weight)"

Struktura exit-4 (escalation chain) bez zmian — nadal potrzebna dla własnej wagi workera.


Zmiana 4 — Grafana panel servers-overview.json

Nowy stat panel “Cloud subagents (Anthropic)”:

SELECT COUNT(*) AS value
FROM dev_r_worker_queue
WHERE job_type = 'subagent' AND status = 'running';

Datasource: Supabase PostgreSQL (grafana_readonly — ma SELECT na dev_r_worker_queue z migration 026). Dodać jako nowy element panels[], skopiować strukturę istniejącego stat panelu.


Weryfikacja

  1. SELECT available_ram_gb('bms-4') z 10 wierszami subagent → ta sama wartość co bez nich (0×10=0)
  2. SELECT * FROM reserve_subagent_slots('s1','subagent',10,'bms-4') → zwraca 10 wierszy nawet przy pełnym RAM
  3. SELECT max_subagents_for_weight('bms-4','orchestrator')12 (emergency_max_workers)
  4. Worker Step 0b PARALLEL-5 → rezerwuje 5 slotów, spawnia 5, brak exit-4
  5. Grafana panel pokazuje > 0 gdy worker aktywnie spawnuje subagenty
  6. SELECT available_ram_gb('bms-4') z 3 light workers + 10 subagents → 22.4 - 9 = 13.4 (subagents nie liczą)