/**
 * Диспетчер циклов осмысления (по образцу OLD_CODE/GO brain/psychic understanding.go + understanding_cycle.go).
 * Один цикл = один процесс осмысления с уникальным ID. По каждому пульсу диспетчер даёт главному циклу
 * один шаг (runOneStep); фоновые циклы — реже. Рекурсивные вызовы ФО заменены на итеративное выполнение
 * через реестр циклов и диспетчер. Без этого невозможна корректная реализация мышления.
 */
(function (global) {
  'use strict';

  /**
   * Учёт «финовых» циклов осмысления (expired главный или завершённый finished).
   * При ConsciousnessFinCyclesTotal > 100 sleep_mode может включить сон даже при критических виталах (контексты переопределяются в prepare).
   */
  function notifyFinCycleCompleted() {
    global.ConsciousnessFinCyclesTotal = (typeof global.ConsciousnessFinCyclesTotal === 'number' ? global.ConsciousnessFinCyclesTotal : 0) + 1;
    if (typeof global.SleepMode_tryAutoAfterFinCycles === 'function') {
      global.SleepMode_tryAutoAfterFinCycles();
    }
  }

  /**
   * Максимум циклов в реестре (память о прошедших мышлениях).
   * Если накопилось более MAX_CYCLES, при появлении нового удаляем самый старый cycle с expired=true.
   * Это — простая и «дубовая» альтернатива конкурентному латеральному торможению.
   */
  var MAX_CYCLES = 100;

  /** Порядок создания цикла (всегда уникален) */
  var curCycleOrder = 0;

  /** Реестр циклов: id -> cycleInfo */
  if (typeof global.ConsciousnessCycles !== 'object' || global.ConsciousnessCycles === null) {
    global.ConsciousnessCycles = Object.create(null);
  }

  /** Счётчик для присвоения ID новому циклу */
  if (typeof global.ConsciousnessProcessNextId !== 'number' || global.ConsciousnessProcessNextId <= 0) {
    global.ConsciousnessProcessNextId = 1;
  }

  /** ID текущего главного цикла (осознаваемого) */
  if (typeof global.ConsciousnessCurrentProcessId !== 'number') {
    global.ConsciousnessCurrentProcessId = 0;
  }

  /**
   * Флаг «главный цикл может модифицировать инфокартину».
   * Для фоновых циклов диспетчер выставляет false перед шагом цикла.
   */
  if (typeof global.ConsciousnessCanModifyInfo !== 'boolean') {
    global.ConsciousnessCanModifyInfo = true;
  }

  /** ID первого созданного главного цикла после загрузки страницы (для условного авто-лога). */
  if (typeof global.ConsciousnessFirstMainCycleId !== 'number') {
    global.ConsciousnessFirstMainCycleId = 0;
  }
  /** См. stimuls_ui / OrientationReflex_getOrReadinessForFinalImageId; выставляется на 2-м пульсе. */
  if (typeof global.OrientationReflex_anyStimulusReadyForOr !== 'boolean') {
    global.OrientationReflex_anyStimulusReadyForOr = false;
  }

  /**
   * Структура цикла осмысления (аналог cycleInfo в Go).
   * @typedef {Object} CycleInfo
   * @property {number} id - уникальный ID цикла
   * @property {number} order - порядок создания (для сортировки/конкуренции)
   * @property {boolean} isMainCycle - главный цикл (осознаваемый)
   * @property {string} status - 'running' | 'finished' | 'cancelled'
   * @property {number} step - номер шага (0 = элементарный проход выполнен, 1+ = мышление)
   * @property {number} count - число вызовов runOneStep диспетчером (не лимит остановки; у фонового цикла шаг реже).
   * @property {number} birthPuls - значение global.cpuls на момент создания цикла (для «возраста» в пульсах).
   * @property {number} weight - значимость (для выбора главного при завершении другого)
   * @property {Object} context - контекст вызова (actualId, branchId, well, emotionId, ...)
   * @property {number} createdAt - timestamp создания
   * @property {number} lastPuls - номер пульса последнего запуска (для очистки старых)
   * @property {boolean} expired - цикл отработал и остаётся как память (не удаляется автоматически)
   * @property {number} noInfoChangeStreak - сколько итераций подряд инфокартина не менялась (для главного цикла)
   * @property {string} lastInfoSig - сигнатура инфокартины на предыдущей итерации
   * @property {Array<Object>} events - важнейшие события процесса (по мере конкретизации)
   * @property {number} createdThemeId - ID темы инфокартины на момент создания цикла
   * @property {number} createdSituationId - ID ситуации инфокартины на момент создания цикла
   * @property {string} thinkingMode - 'target' | 'passive' — режим мышления цикла (не хранится в инфокартине).
   * @property {number} goalImageId - ID образа цели (GoalImagesLife); при thinkingMode passive — 0; при target — currentGoalId
   * @property {number} waitingFeedbackAutomatizmId - ID автоматизма, запущенного на 1-м уровне ФО; ожидается оценка effect в кадре ЭП (0 — нет)
   * @property {number} lastGestaltConsciencePuls - номер пульса последнего тика ур.4/гештальта (Gestalt_onConsciousnessTick) для этого цикла; 0 — не было
   * @property {boolean} passiveWorkAfterGestaltExpire - true, если цикл ушёл в expired сразу после тика гештальта на том же пульсе (стагнация) и инфокартина переведена в passive
   * @property {Array<Object>} fantasizmArr - снимки правил ЭП (кадры); синхронизируется с шагами info_fantasizming
   * @property {boolean} fantasizmPassiveFillStarted - зарезервировано (раньше — первичное заполнение)
   * @property {Object} fantazmingPlot - { chain: Array, lastGeneratedActionId }: chain — звенья { fromStimulusId, actionImageId,
   *   episodeIndex, effect }; lastGeneratedActionId — последний ОД шага (на следующем пульсе ищутся кадры ЭП с
   *   responseStimulusImageId, совпадающим с этим id — см. info_fantasizming).
   * @property {number} currentFantazmingStimulus - дубль фокуса для отладки/UI: как правило тот же id, что lastGeneratedActionId
   *   после успешного шага.
   * @property {number} passiveFantasyNoChangeStreak - сколько раз подряд сигнатура инфокартины не изменилась на пассивном
   *   шаге (сравнение sig до и после info_fantasizming в runOneStep); при ≥3 и совпадении с passiveFantasyLastSig
   *   фантазм на пульс не вызывается.
   * @property {string} passiveFantasyLastSig - снимок Consciousness_getInfoPictureSignature() после последнего пассивного шага
   */
  function snapshotGoalImageIdForCycle() {
    var info = global.InfoPicture;
    if (!info || typeof info !== 'object') return 0;
    var main = getMainCycle();
    if (main && main.thinkingMode === 'passive') return 0;
    return typeof info.currentGoalId === 'number' ? info.currentGoalId : 0;
  }

  function createCycleInfo(id, context) {
    curCycleOrder += 1;
    var weight = 0;
    if (context && typeof context.significance === 'number') {
      weight = Math.abs(context.significance);
    }
    var info = global.InfoPicture;
    var createdThemeId = 0;
    var createdSituationId = 0;
    if (info && typeof info === 'object') {
      createdThemeId = typeof info.themeId === 'number' ? info.themeId : (info.themeId || 0);
      createdSituationId = typeof info.situationId === 'number' ? info.situationId : (info.situationId || 0);
    }
    return {
      id: id,
      order: curCycleOrder,
      isMainCycle: true,
      status: 'running',
      step: 0,
      count: 0,
      weight: weight,
      createdThemeId: createdThemeId,
      createdSituationId: createdSituationId,
      goalImageId: snapshotGoalImageIdForCycle(),
      thinkingMode: 'target',
      waitingFeedbackAutomatizmId: 0,
      awaitingAutomatismEpisodeFeedback: false,
      automatismOperatorAnswerReceived: false,
      automatismAnswerLevel2Done: false,
      lastAutomatismEpisodeEffect: null,
      context: context || {},
      createdAt: typeof Date.now === 'function' ? Date.now() : 0,
      lastPuls: 0,
      birthPuls: typeof global.cpuls === 'number' ? global.cpuls : 0,
      expired: false,
      noInfoChangeStreak: 0,
      lastInfoSig: '',
      events: []
      ,
      lastGestaltConsciencePuls: 0,
      passiveWorkAfterGestaltExpire: false,
      // Человекочитаемый лог работы цикла (для оператора).
      debugLog: [],
      // Если оператор закрыл окно лога — больше не показывать авто-лог для этого цикла.
      debugLogSuppressed: false,
      fantasizmArr: [],
      fantasizmPassiveFillStarted: false,
      /** Сюжет фантазирования: цепочка звеньев { fromStimulusId, actionImageId, episodeIndex, effect }; lastGeneratedActionId — якорь следующего поиска в ЭП. */
      fantazmingPlot: { chain: [], lastGeneratedActionId: 0 },
      /** Последний фокус фантазии (для отображения/отладки; обычно совпадает с lastGeneratedActionId после шага). */
      currentFantazmingStimulus: 0,
      /** Сколько пульсов подряд сигнатура инфокартины не менялась на пассивном шаге (антистагнация вместе с passiveFantasyLastSig). */
      passiveFantasyNoChangeStreak: 0,
      /** Последняя сигнатура инфокартины после обработки пассивной ветки runOneStep. */
      passiveFantasyLastSig: ''
    };
  }

  function escapeHtml(s) {
    s = String(s == null ? '' : s);
    return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  }

  function cycleAppendLog(cycle, text, pulsCount, opts) {
    if (!cycle) return;
    opts = opts && typeof opts === 'object' ? opts : {};
    // После expired лог больше не продолжаем (кроме принудительных системных сообщений).
    if (cycle.expired && !opts.force) return;
    if (!Array.isArray(cycle.debugLog)) cycle.debugLog = [];
    var msg = (typeof text === 'string') ? text : String(text);
    cycle.debugLog.push({
      atPuls: typeof pulsCount === 'number' ? pulsCount : 0,
      atMs: typeof Date.now === 'function' ? Date.now() : 0,
      text: msg
    });
    // ограничим размер, чтобы не разрастался
    if (cycle.debugLog.length > 200) {
      cycle.debugLog.splice(0, cycle.debugLog.length - 200);
    }
  }

  function formatCycleLogHtml(cycle) {
    if (!cycle || !Array.isArray(cycle.debugLog) || !cycle.debugLog.length) {
      return '<i>лог пуст</i>';
    }
    var lines = [];
    var from = Math.max(0, cycle.debugLog.length - 40);
    for (var i = from; i < cycle.debugLog.length; i++) {
      var e = cycle.debugLog[i];
      var p = (e && typeof e.atPuls === 'number') ? e.atPuls : 0;
      var t = e && e.text != null ? e.text : '';
      lines.push('[' + String(p) + '] ' + String(t));
    }
    return '<pre style="white-space:pre-wrap;margin:6px 0 0 0;font-size:10pt">' + escapeHtml(lines.join('\n')) + '</pre>';
  }

  function maybeShowMainCycleLog(main, pulsCount) {
    if (!main || !main.isMainCycle || main.status !== 'running') return;
    if (main.debugLogSuppressed) return;
    if (main.expired) return;
    var isFirstMainAfterLoad = (global.ConsciousnessFirstMainCycleId > 0 && main.id === global.ConsciousnessFirstMainCycleId);
    if (isFirstMainAfterLoad && !global.OrientationReflex_anyStimulusReadyForOr) return;
    if (typeof global.show_alert !== 'function') return;
    var curLen = Array.isArray(main.debugLog) ? main.debugLog.length : 0;
    if (typeof main.debugLogLastShownLen === 'number' && main.debugLogLastShownLen === curLen) {
      return;
    }
    main.debugLogLastShownLen = curLen;
    // Не мешаем другим модальным окнам.
    if (typeof global.Alert_getActiveDialogType === 'function') {
      var tp = global.Alert_getActiveDialogType();
      if (tp && tp !== 'cyclelog') return;
    }
    var stimId = (main.context && typeof main.context.actual_stimul_ID === 'number') ? main.context.actual_stimul_ID : 0;
    var title = '<b>Главный цикл #' + String(main.id) + '</b>' +
      (stimId ? (' (стимул FinalImageId=' + String(stimId) + ')') : '') +
      (main.expired ? ' <b>expired</b>' : '');
    var body = title +
      '<br><small>Окно обновляется каждый пульс. Закроете — для этого цикла больше не показывается.</small>' +
      '<br>' + formatCycleLogHtml(main);
    global.show_alert(body, 0, {
      dialogType: 'cyclelog',
      onClose: function (info) {
        if (info && info.userInitiated) {
          main.debugLogSuppressed = true;
        }
      }
    });
  }

  /**
   * Оперативная память фоновых циклов: после решения проблемы / исчерпания сценария
   * значимость (weight) всех не-главных running-циклов умножается на factor (по умолчанию 0.5).
   */
  function decayBackgroundOperativeMemory(factor) {
    factor = typeof factor === 'number' && factor > 0 && factor < 1 ? factor : 0.5;
    var registry = global.ConsciousnessCycles;
    for (var k in registry) {
      if (!Object.prototype.hasOwnProperty.call(registry, k)) continue;
      var c = registry[k];
      if (!c || c.isMainCycle || c.status !== 'running') continue;
      var w = typeof c.weight === 'number' ? c.weight : 0;
      c.weight = Math.max(0.01, w * factor);
    }
  }

  function addCycleEvent(cycle, type, data, pulsCount) {
    if (!cycle) return;
    if (!Array.isArray(cycle.events)) cycle.events = [];
    cycle.events.push({
      atPuls: typeof pulsCount === 'number' ? pulsCount : 0,
      atMs: typeof Date.now === 'function' ? Date.now() : 0,
      type: type || 'event',
      data: data || null
    });
    // ограничим журнал событий, чтобы не разрастался бесконечно
    if (cycle.events.length > 50) {
      cycle.events.splice(0, cycle.events.length - 50);
    }
  }

  function getInfoPictureSignature() {
    var info = global.InfoPicture;
    if (!info || typeof info !== 'object') return '';
    // Не включаем thinkingImportance/thinkingProcessId — они меняются при переключении циклов.
    var snap = {
      themeId: info.themeId || 0,
      situationId: info.situationId || 0,
      problemFinalImageId: info.problemFinalImageId || 0,
      step: info.step || 0,
      stimulusFinalImageId: info.stimulus && info.stimulus.finalImageId ? info.stimulus.finalImageId : 0,
      tree: info.tree ? [
        info.tree.baseID || 0,
        info.tree.emotionID || 0,
        info.tree.activityID || 0,
        info.tree.toneMoodID || 0,
        info.tree.simbolID || 0,
        info.tree.phraseID || 0
      ] : [0, 0, 0, 0, 0, 0]
    };
    // Детекторы: все числовые поля, кроме перечисленных выше и служебных.
    for (var k in info) {
      if (!Object.prototype.hasOwnProperty.call(info, k)) continue;
      if (k === 'themeId' || k === 'situationId' || k === 'problemFinalImageId' || k === 'step' ||
          k === 'tree' || k === 'stimulus' || k === 'thinkingImportance' || k === 'thinkingProcessId' ||
          k === 'currentGoalType' || k === 'currentGoalId' || k === 'currentGoalVitalId') {
        continue;
      }
      var v = info[k];
      if (typeof v === 'number') {
        snap[k] = v;
      }
    }
    try {
      return JSON.stringify(snap);
    } catch (e) {
      return '';
    }
  }

  function pruneCyclesIfNeeded(pulsCount) {
    var registry = global.ConsciousnessCycles;
    var keys = Object.keys(registry);
    if (keys.length <= MAX_CYCLES) return;

    // Сначала удаляем самый старый expired=true.
    var oldestExpired = null;
    for (var i = 0; i < keys.length; i++) {
      var c = registry[keys[i]];
      if (!c || !c.expired) continue;
      if (!oldestExpired || (c.order || 0) < (oldestExpired.order || 0)) oldestExpired = c;
    }
    if (oldestExpired) {
      deleteCycle(oldestExpired.id);
      return;
    }

    // Если expired ещё нет — удаляем самый старый не главный цикл (крайняя мера, чтобы не блокировать создание нового).
    var oldestBg = null;
    for (var j = 0; j < keys.length; j++) {
      var c2 = registry[keys[j]];
      if (!c2 || c2.isMainCycle) continue;
      if (!oldestBg || (c2.order || 0) < (oldestBg.order || 0)) oldestBg = c2;
    }
    if (oldestBg) {
      deleteCycle(oldestBg.id);
    }
  }

  /**
   * Создать новый цикл осмысления и зарегистрировать его.
   * @param {Object} context - контекст (actualId, branchId, significance, ...)
   * @returns {CycleInfo|null} созданный цикл или null при переполнении
   */
  function createConsciousnessCycle(context) {
    var registry = global.ConsciousnessCycles;
    var id = global.ConsciousnessProcessNextId++;
    var cycle = createCycleInfo(id, context);
    registry[id] = cycle;
    if (cycle.isMainCycle && !(global.ConsciousnessFirstMainCycleId > 0)) {
      global.ConsciousnessFirstMainCycleId = id;
    }
    addCycleEvent(cycle, 'created', { context: context || null }, 0);
    cycleAppendLog(cycle, 'Создан цикл. Стимул FinalImageId=' + String(context && context.actual_stimul_ID ? context.actual_stimul_ID : 0) + '.', typeof global.cpuls === 'number' ? global.cpuls : 0);
    pruneCyclesIfNeeded(typeof global.cpuls === 'number' ? global.cpuls : 0);
    return cycle;
  }

  /**
   * Установить цикл с заданным ID главным. Остальные получают isMainCycle = false.
   * @param {number} cycleId - ID цикла
   */
  function setMainCycle(cycleId) {
    var registry = global.ConsciousnessCycles;
    for (var k in registry) {
      if (Object.prototype.hasOwnProperty.call(registry, k)) {
        registry[k].isMainCycle = (Number(k) === Number(cycleId));
      }
    }
    global.ConsciousnessCurrentProcessId = cycleId;
  }

  /**
   * Получить текущий главный цикл (со статусом running).
   * @returns {CycleInfo|null}
   */
  function getMainCycle() {
    var registry = global.ConsciousnessCycles;
    for (var k in registry) {
      if (!Object.prototype.hasOwnProperty.call(registry, k)) continue;
      var c = registry[k];
      if (c && c.isMainCycle && c.status === 'running') return c;
    }
    return null;
  }

  /**
   * Получить цикл по ID.
   * @param {number} id
   * @returns {CycleInfo|null}
   */
  function getCycle(id) {
    var c = global.ConsciousnessCycles[Number(id)];
    return c || null;
  }

  /**
   * Отменить цикл (установить status = 'cancelled'). RunOneStep будет его игнорировать.
   * @param {number} id
   */
  function cancelConsciousnessProcess(id) {
    var c = getCycle(id);
    if (c) c.status = 'cancelled';
  }

  /**
   * Удалить цикл из реестра. Если удалялся главный — выбрать нового главного по весу (макс. weight среди running).
   * @param {number} id
   */
  function deleteCycle(id) {
    var registry = global.ConsciousnessCycles;
    var wasMain = false;
    var cycle = registry[id];
    if (cycle) {
      wasMain = cycle.isMainCycle;
      delete registry[id];
    }
    if (wasMain) {
      var nextMain = null;
      var maxWeight = -1;
      for (var k in registry) {
        if (!Object.prototype.hasOwnProperty.call(registry, k)) continue;
        var c = registry[k];
        if (c && c.status === 'running' && c.weight > maxWeight) {
          maxWeight = c.weight;
          nextMain = c;
        }
      }
      if (nextMain) {
        setMainCycle(nextMain.id);
      } else {
        global.ConsciousnessCurrentProcessId = 0;
      }
    }
  }

  /**
   * Удалить все фоновые циклы (running, isMainCycle=false). Очередь прерываний очищается от несуществующих ID.
   * Вызывается только из SleepMode_exit(true) — завершение сновидений по лимиту кадров; ручной выход из сна фон не трогает.
   */
  function deleteAllBackgroundCycles() {
    var registry = global.ConsciousnessCycles;
    var ids = [];
    for (var kb in registry) {
      if (!Object.prototype.hasOwnProperty.call(registry, kb)) continue;
      var cb = registry[kb];
      if (cb && !cb.isMainCycle && cb.status === 'running') ids.push(cb.id);
    }
    for (var i = 0; i < ids.length; i++) {
      deleteCycle(ids[i]);
    }
    var stack = global.ConsciousnessBackgroundStack;
    if (Array.isArray(stack) && stack.length) {
      var filtered = [];
      for (var s = 0; s < stack.length; s++) {
        var sid = stack[s];
        if (registry[sid] != null) filtered.push(sid);
      }
      global.ConsciousnessBackgroundStack = filtered;
    }
  }

  /**
   * Выполнить один шаг цикла. Вызывает глобальную Consciousness_runOneStep(cycle), если она задана.
   * @param {CycleInfo} cycle
   * @param {number} pulsCount - текущий номер пульса
   */
  function runOneStep(cycle, pulsCount) {
    if (!cycle || cycle.status !== 'running') return;
    cycle.lastPuls = pulsCount;
    cycle.count += 1;
    if (typeof global.Consciousness_runOneStep === 'function') {
      global.Consciousness_runOneStep(cycle);
    }
  }

  /**
   * Поднять фоновый цикл в главный: сначала по очереди прерываний (стек ID), иначе по макс. weight.
   * Только сменить вывеску и дать права редактировать инфокартину; ID цикла не меняется.
   */
  function promoteFromBackgroundCycle() {
    var stack = global.ConsciousnessBackgroundStack;
    if (Array.isArray(stack) && stack.length > 0) {
      while (stack.length > 0) {
        var id = stack.pop();
        var c = getCycle(id);
        if (c && c.status === 'running' && !c.expired) {
          setMainCycle(c.id);
          if (typeof global.InfoPicture !== 'undefined' && global.InfoPicture) {
            global.InfoPicture.thinkingImportance = c.weight;
            global.InfoPicture.thinkingProcessId = c.id;
            if (typeof global.Info_updateView === 'function') global.Info_updateView();
          }
          if (typeof global.show_alert === 'function') {
            var stimId = (c.context && typeof c.context.actual_stimul_ID === 'number') ? c.context.actual_stimul_ID : 0;
            global.show_alert(
              'Возврат к прерванному процессу ID=' + String(c.id) + ' в главный (стимул FinalImageId=' + String(stimId) + ', значимость=' + String(c.weight || 0) + ').',
              1500
            );
          }
          return;
        }
      }
    }
    var registry = global.ConsciousnessCycles;
    var best = null;
    var bestWeight = -1;
    for (var k in registry) {
      if (!Object.prototype.hasOwnProperty.call(registry, k)) continue;
      var c = registry[k];
      if (!c || c.isMainCycle || c.status !== 'running' || c.expired) continue;
      var w = typeof c.weight === 'number' ? c.weight : 0;
      if (w > bestWeight) {
        bestWeight = w;
        best = c;
      }
    }
    if (!best) return;
    setMainCycle(best.id);
    if (typeof global.InfoPicture !== 'undefined' && global.InfoPicture) {
      global.InfoPicture.thinkingImportance = best.weight;
      global.InfoPicture.thinkingProcessId = best.id;
      if (typeof global.Info_updateView === 'function') global.Info_updateView();
    }
    if (typeof global.show_alert === 'function') {
      var stimId = (best.context && typeof best.context.actual_stimul_ID === 'number') ? best.context.actual_stimul_ID : 0;
      global.show_alert(
        'Возврат к фоновому процессу ID=' + String(best.id) + ' в главный (стимул FinalImageId=' + String(stimId) + ', значимость=' + String(best.weight || 0) + ').',
        1500
      );
    }
  }

  /**
   * Поднять контекст из стека фоновых (устаревший путь: создание нового цикла по сохранённому контексту).
   * Оставлено для совместимости; основной путь — promoteFromBackgroundCycle().
   */
  function promoteFromBackgroundStack() {
    var stack = global.ConsciousnessBackgroundStack;
    if (!Array.isArray(stack) || stack.length === 0) return;
    var bg = stack.pop();
    if (!bg || !bg.context) return;
    var cycle = createConsciousnessCycle(bg.context);
    if (!cycle) return;
    setMainCycle(cycle.id);
    if (typeof global.InfoPicture !== 'undefined' && global.InfoPicture && bg.importance != null) {
      global.InfoPicture.thinkingImportance = bg.importance;
      global.InfoPicture.thinkingProcessId = cycle.id;
      if (typeof global.Info_updateView === 'function') global.Info_updateView();
    }
    if (typeof global.Consciousness_runElementary === 'function') {
      global.Consciousness_runElementary(cycle);
    }
  }

  /**
   * Поддержание реестра циклов: фоновые циклы автоматически не закрываем (это память).
   * Единственная «очистка» — ограничение размера реестра через pruneCyclesIfNeeded().
   * @param {number} pulsCount
   */
  function cleanupCycles(pulsCount) {
    pruneCyclesIfNeeded(pulsCount);
  }

  /**
   * Диспетчер циклов мышления. Вызывается по каждому пульсу.
   * 1) Если нет главного цикла — поднять один фоновый в главный (promoteFromBackgroundCycle).
   * 2) Главному циклу выполнить один шаг (Consciousness_runOneStep).
   * 3) Если после шага главный завершён — удалить его, поднять следующий фоновый в главный.
   * 4) Фоновым циклам (running, не main) дать один шаг раз в 5 пульсов; ID сохраняются.
   * 5) Очистить завершённые/отменённые и старые фоновые.
   * @param {number} pulsCount - текущее значение счётчика пульсов (например global.cpuls)
   */
  function dispetchConsciousnessThinking(pulsCount) {
    pulsCount = typeof pulsCount === 'number' ? pulsCount : 0;

    var main = getMainCycle();

    if (!main) {
      promoteFromBackgroundCycle();
      cleanupCycles(pulsCount);
      return;
    }

    // Главный цикл имеет право модифицировать инфокартину.
    global.ConsciousnessCanModifyInfo = true;
    // Для логики expired: снимок инфокартины до/после итерации главного цикла.
    var sigBefore = getInfoPictureSignature();
    runOneStep(main, pulsCount);
    var sigAfter = getInfoPictureSignature();
    if (sigBefore && sigAfter && sigBefore === sigAfter) {
      main.noInfoChangeStreak = (main.noInfoChangeStreak || 0) + 1;
    } else {
      main.noInfoChangeStreak = 0;
    }
    main.lastInfoSig = sigAfter;
    var skipStagnationExpire = !!(main.awaitingAutomatismEpisodeFeedback && !main.automatismAnswerLevel2Done);
    if (!main.expired && main.noInfoChangeStreak >= 3 && !skipStagnationExpire) {
      cycleAppendLog(main, 'Цикл помечен expired (3 итерации без изменений инфокартины).', pulsCount, { force: true });
      main.expired = true;
      notifyFinCycleCompleted();
      main.isMainCycle = false; // чтобы getMainCycle() больше не возвращал этот цикл — простаивающий цикл перестаёт получать шаги
      addCycleEvent(main, 'expired', { reason: '3 iterations without InfoPicture changes' }, pulsCount);
      var gestaltTickSamePuls = (typeof main.lastGestaltConsciencePuls === 'number' &&
        main.lastGestaltConsciencePuls === pulsCount);
      if (gestaltTickSamePuls) {
        main.passiveWorkAfterGestaltExpire = true;
        addCycleEvent(main, 'passiveAfterGestaltStagnation', { puls: pulsCount }, pulsCount);
        main.thinkingMode = 'passive';
        if (typeof global.Info_updateView === 'function') global.Info_updateView();
        cycleAppendLog(main, 'После тика ур.4/гештальта: expired по стагнации — главный цикл переведён в пассивный режим мышления (thinkingMode).', pulsCount, { force: true });
      }
      decayBackgroundOperativeMemory(0.5);
      var mw = typeof main.weight === 'number' ? main.weight : 0;
      main.weight = Math.max(0.01, mw * 0.5);
      if (typeof global.InfoPicture !== 'undefined' && global.InfoPicture) {
        global.InfoPicture.thinkingImportance = main.weight;
        if (typeof global.Info_updateView === 'function') global.Info_updateView();
      }
    }

    // Детектор «Неудовлетворённость существующим»:
    // если главный цикл expired и более 20 пульсов подряд активен базовый контекст «Покой» (id=14),
    // то включаем InfoPicture.isDissatisfaction (и dissatisfaction) через InfoDet_updateDissatisfaction.
    (function () {
      var info = global.InfoPicture;
      var peaceActive = Array.isArray(global.BasicContextsActived) && global.BasicContextsActived.indexOf(14) !== -1;
      if (main && main.expired && peaceActive) {
        main.expiredPeacePulses = (typeof main.expiredPeacePulses === 'number' ? main.expiredPeacePulses : 0) + 1;
      } else if (main) {
        main.expiredPeacePulses = 0;
      }
      var should = !!(main && main.expiredPeacePulses > 20);
      if (typeof global.InfoDet_updateDissatisfaction === 'function') {
        global.InfoDet_updateDissatisfaction(should);
      } else if (info) {
        info.dissatisfaction = should ? 1 : 0;
        info.isDissatisfaction = should ? 1 : 0;
        if (typeof global.Info_updateView === 'function') global.Info_updateView();
      }
    })();

    // Детектор «Непонимание»:
    // целевой режим мышления главного цикла + слишком длинная итерация или главный expired в целевом режиме.
    (function () {
      var info = global.InfoPicture;
      var isTarget = !!(main && main.thinkingMode !== 'passive');
      var tooLong = !!(main && typeof main.count === 'number' && main.count > 10);
      var expiredTarget = !!(main && main.expired && isTarget);
      var flag = !!(isTarget && (tooLong || expiredTarget));
      if (typeof global.InfoDet_updateMisunderstanding === 'function') {
        global.InfoDet_updateMisunderstanding(flag);
      } else if (info) {
        info.misunderstanding = flag ? 1 : 0;
        if (typeof global.Info_updateView === 'function') global.Info_updateView();
      }
    })();

    // Авто-показ/обновление лога главного цикла (если оператор не закрыл для этого цикла).
    maybeShowMainCycleLog(main, pulsCount);

    if (main.status === 'finished' || main.status === 'cancelled') {
      addCycleEvent(main, main.status, null, pulsCount);
      cycleAppendLog(main, 'Цикл завершён: ' + String(main.status) + '.', pulsCount, { force: true });
      if (main.status === 'finished') {
        notifyFinCycleCompleted();
      }
      decayBackgroundOperativeMemory(0.5);
      deleteCycle(main.id);
      promoteFromBackgroundCycle();
    }

    var registry = global.ConsciousnessCycles;
    for (var k in registry) {
      if (!Object.prototype.hasOwnProperty.call(registry, k)) continue;
      var c = registry[k];
      if (!c || c.isMainCycle || c.status !== 'running') continue;
      var fastBgAwait = !!(c.awaitingAutomatismEpisodeFeedback && c.automatismOperatorAnswerReceived && !c.automatismAnswerLevel2Done);
      if (fastBgAwait || pulsCount % 5 === 0) {
        // Фоновые циклы могут только читать инфокартину, без модификаций.
        global.ConsciousnessCanModifyInfo = false;
        runOneStep(c, pulsCount);
        // Сюжет фонового фантазирования — в info_fantasizming (conscious_attention_channel, пассивный режим).
        // Фоновые циклы не закрываем автоматически — это память о прошедших мышлениях.
      }
    }

    cleanupCycles(pulsCount);
  }

  /**
   * Число фоновых циклов (running, isMainCycle=false) в реестре — для индикации «фон: N».
   * @returns {number}
   */
  function getBackgroundCyclesCount() {
    var registry = global.ConsciousnessCycles;
    var n = 0;
    for (var k in registry) {
      if (!Object.prototype.hasOwnProperty.call(registry, k)) continue;
      var c = registry[k];
      if (c && !c.isMainCycle && c.status === 'running') n += 1;
    }
    return n;
  }

  /**
   * Список фоновых циклов (для UI: детализация по клику «фон: N»).
   * @returns {Array<CycleInfo>}
   */
  function getBackgroundCyclesList() {
    var registry = global.ConsciousnessCycles;
    var list = [];
    for (var k in registry) {
      if (!Object.prototype.hasOwnProperty.call(registry, k)) continue;
      var c = registry[k];
      if (c && !c.isMainCycle && c.status === 'running') list.push(c);
    }
    list.sort(function (a, b) { return (b.weight || 0) - (a.weight || 0); });
    return list;
  }

  /**
   * Можно ли в текущем шаге цикла модифицировать инфокартину (InfoPicture)?
   * true — для главного цикла; false — для фонового шага.
   * Код внутри функций уровней осознания должен учитывать этот флаг.
   * @returns {boolean}
   */
  function canModifyInfoPicture() {
    return !!global.ConsciousnessCanModifyInfo;
  }

  /**
   * Режим мышления текущего главного цикла ('target' | 'passive'). Без главного — 'target'.
   * Режим не хранится в InfoPicture; инфокартина получает только следы (сюжет фантазма и т.д.).
   * @returns {'target'|'passive'}
   */
  function getMainCycleThinkingMode() {
    var c = getMainCycle();
    if (!c) return 'target';
    return c.thinkingMode === 'passive' ? 'passive' : 'target';
  }

  global.Consciousness_createCycle = createConsciousnessCycle;
  global.Consciousness_setMainCycle = setMainCycle;
  global.Consciousness_getMainCycle = getMainCycle;
  global.Consciousness_getMainCycleThinkingMode = getMainCycleThinkingMode;
  global.Consciousness_getCycle = getCycle;
  global.Consciousness_cancelProcess = cancelConsciousnessProcess;
  global.Consciousness_deleteCycle = deleteCycle;
  global.Consciousness_deleteAllBackgroundCycles = deleteAllBackgroundCycles;
  global.Consciousness_promoteFromBackgroundCycle = promoteFromBackgroundCycle;
  global.Consciousness_promoteFromBackgroundStack = promoteFromBackgroundStack;
  global.Consciousness_getBackgroundCyclesCount = getBackgroundCyclesCount;
  global.Consciousness_getBackgroundCyclesList = getBackgroundCyclesList;
  global.Consciousness_canModifyInfoPicture = canModifyInfoPicture;
  global.Consciousness_decayBackgroundOperativeMemory = decayBackgroundOperativeMemory;
  global.Consciousness_cycleAppendLog = function (cycle, text, pulsCount, opts) {
    cycleAppendLog(cycle, text, pulsCount, opts);
  };
  global.dispetchConsciousnessThinking = dispetchConsciousnessThinking;
  global.Consciousness_getInfoPictureSignature = getInfoPictureSignature;
})(window);
