process.env.WALL_E_PROCESS_ROLE = process.env.WALL_E_PROCESS_ROLE || 'walle-daemon';
// Refuse to boot under a Node major that mismatches the pinned one BEFORE brain
// (and its native better-sqlite3) loads. No-op in shipped installs.
require('../claude-task-manager/lib/node-pin-guard').assertPinnedNode();
try {
  if (process.env.WALL_E_STORAGE_MIGRATION_BYPASS !== '1' && process.env.CTM_STORAGE_MIGRATION_BYPASS !== '1') {
    const guard = require('../claude-task-manager/lib/storage-migration').startupGuard('Wall-E', process.env);
    if (!guard.ok) {
      console.error('[storage-migration] Wall-E startup paused while storage migration is active.');
      process.exit(75);
    } else if (guard.stale) {
      console.warn('[storage-migration] Ignoring stale storage migration lock.');
    }
  }
} catch (e) {
  console.warn('[storage-migration] Startup guard failed open:', e.message);
}
const { processTitle: formatProcessTitle } = require('../claude-task-manager/lib/process-title');

const brain = require('./brain'); // brain.js loads .env from project root
const ingest = require('./loops/ingest');
const think = require('./loops/think');
const reflect = require('./loops/reflect');
const { runDueSkills } = require('./skills/skill-planner');
const { loadAllSkills } = require('./skills/skill-loader');
const { runDueTasks, recoverInterruptedTasks } = require('./loops/tasks');
// Adapters are loaded via the adapter registry (Item D of plugin-sdk
// deep-dive). Drop `<name>.js` + `<name>.plugin.json` into wall-e/adapters/
// and the bootstrap pass picks it up. mode=poll = live loop adapter;
// mode=import = one-shot import surface (separate UI, not in agent loop).
const adapterRegistry = require('./adapters/registry');
// Channels are loaded via the channel registry (Item C of plugin-sdk
// deep-dive). Adding a new channel = drop `<name>-channel.js` +
// `<name>-channel.plugin.json` into wall-e/channels/. No edits here.
const channelRegistry = require('./channels/registry');
const { chat: walleChat } = require('./chat');
const telemetry = require('./telemetry');
const fs = require('fs');
const path = require('path');

// Prevent unhandled errors from silently killing the daemon
const CRASH_LOG_DIR = path.join(process.env.HOME || '/tmp', '.walle', 'logs');
const CRASH_LOG = path.join(CRASH_LOG_DIR, 'crash.log');
function _writeCrashLog(label, err) {
  try {
    fs.mkdirSync(CRASH_LOG_DIR, { recursive: true });
    const line = `[${new Date().toISOString()}] ${label}: ${err instanceof Error ? err.stack : String(err)}\n`;
    fs.appendFileSync(CRASH_LOG, line);
  } catch {}
}
// Process tagging (discoverable via pgrep -f 'Wall-E dev-4500')
process.title = formatProcessTitle('walle', process.env.WALLE_INSTANCE_TAG || 'primary');

let _epipeSeen = false; // prevent EPIPE cascade: console.error in handler throws EPIPE → retriggers handler
process.on('uncaughtException', (err) => {
  // EPIPE/EAGAIN from broken stdout/stderr pipe — silently ignore to prevent infinite cascade
  if (err && (err.code === 'EPIPE' || err.code === 'EAGAIN' || err.errno === -11)) {
    if (!_epipeSeen) { _epipeSeen = true; _writeCrashLog('PIPE ERROR (suppressing cascade)', err); }
    return;
  }
  _writeCrashLog('UNCAUGHT EXCEPTION', err);
  try { console.error('[wall-e] UNCAUGHT EXCEPTION (keeping alive):', err); } catch {}
  try { telemetry.track('error', { type: 'uncaught', message: err.message, stack: (err.stack || '').slice(0, 500) }); } catch {}
});
process.on('unhandledRejection', (reason) => {
  _writeCrashLog('UNHANDLED REJECTION', reason);
  try { console.error('[wall-e] UNHANDLED REJECTION (keeping alive):', reason); } catch {}
  try { telemetry.track('error', { type: 'unhandled_rejection', message: String(reason).slice(0, 300) }); } catch {}
});

const CONFIG_PATH = path.join(__dirname, 'wall-e-config.json');
const MIN_INTERVAL_MS = 10000; // 10 seconds minimum
const MAX_INTERVAL_MS = 3600000; // 1 hour maximum

const { Scheduler } = require('./lib/scheduler');
let lastLearnerRun = 0;
let shuttingDown = false;

function loadOrCreateConfig() {
  if (fs.existsSync(CONFIG_PATH)) {
    const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
    // Sync owner name from env if config still has placeholder
    const envName = process.env.WALLE_OWNER_NAME;
    if (envName && config.owner && (config.owner.name === 'Unknown' || !config.owner.name)) {
      const parts = envName.split(/\s+/);
      config.owner.name = envName;
      config.owner.first_name = parts[0];
      config.owner.last_name = parts.slice(1).join(' ');
      fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
      console.log(`[wall-e] Updated config owner from WALLE_OWNER_NAME env var.`);
    }
    return config;
  }
  // No config file — create from env or defaults
  const envName = process.env.WALLE_OWNER_NAME || 'Unknown';
  const parts = envName.split(/\s+/);
  const config = {
    owner: {
      name: envName,
      first_name: parts[0],
      last_name: parts.slice(1).join(' '),
      timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
    },
    adapters: { ctm: { enabled: true }, slack: { enabled: !!process.env.SLACK_TOKEN } },
    intervals: { ingest_ms: 60000, think_ms: 120000 },
    channels: {
      imessage: { enabled: false, buddy_id: '' },
      slack_dm: { enabled: false, bot_token: '' },
      telegram: { enabled: false, bot_token: '', owner_chat_id: null },
    },
  };
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
  console.log(`[wall-e] Created config at ${CONFIG_PATH}`);
  return config;
}

function clampInterval(value, fallback) {
  const n = Number(value) || fallback;
  return Math.max(MIN_INTERVAL_MS, Math.min(MAX_INTERVAL_MS, n));
}

function bootstrapSkills() {
  const existing = brain.listSkills({});
  if (existing.length > 0) return; // Already bootstrapped

  brain.insertSkill({
    name: 'scan-ctm-sessions',
    description: 'Scan Claude Code session files for new conversations',
    trigger_type: 'interval',
    trigger_config: JSON.stringify({ interval_ms: 60000 }),
    prompt_template: 'INTERNAL_SKILL:scan-ctm-sessions',
    execution: 'script',
  });

  brain.insertSkill({
    name: 'learn-claude-code-skills',
    description: 'Read Claude Code skills and MCP config to learn what tools are available',
    trigger_type: 'interval',
    trigger_config: JSON.stringify({ interval_ms: 3600000 }), // hourly
    prompt_template: 'Read the Claude Code configuration at ~/.claude/settings.json and skill files at ~/.claude/skills/ to understand what MCP servers and skills are available. Summarize the capabilities as observations.',
  });

  brain.insertSkill({
    name: 'ingest-slack-history',
    description: 'Systematically pull all Slack conversation history (DMs, channels) going back 4 years',
    trigger_type: 'interval',
    trigger_config: JSON.stringify({ interval_ms: 30000 }), // Every 30s when active
    prompt_template: 'INTERNAL_SKILL:slack-ingest', // Special marker -- not a Claude prompt
    enabled: 0, // Disabled by default; start via UI or API
  });

  console.log('[wall-e] Bootstrapped initial skills');
}

/**
 * Ensure all bundled filesystem skills are registered in the DB.
 * bootstrapSkills() only runs on first boot (empty DB), so new bundled skills
 * added later never get registered. This syncs any missing ones on every startup.
 */
function syncBundledSkills() {
  const filesystemSkills = loadAllSkills();
  const dbSkills = brain.listSkills({});
  const dbByName = new Map(dbSkills.map(s => [s.name, s]));
  const dbNames = new Set(dbByName.keys());

  let added = 0;
  let updated = 0;
  for (const skill of filesystemSkills) {
    const triggerType = (skill.trigger && skill.trigger.type) || skill.execution || 'manual';
    const triggerConfig = skill.trigger && skill.trigger.interval_ms
      ? JSON.stringify({ interval_ms: skill.trigger.interval_ms })
      : null;
    const promptTemplate = skill.execution === 'script'
      ? `INTERNAL_SKILL:${skill.name}`
      : skill.instructions || '';

    if (dbNames.has(skill.name)) {
      const existing = dbByName.get(skill.name);
      // Upgrade legacy prompt-based CTM scanning to the deterministic script
      // path. Session continuity must not depend on live LLM/network access.
      if (skill.name === 'scan-ctm-sessions' && skill.execution === 'script' && existing) {
        const updates = {};
        if (existing.execution !== 'script') updates.execution = 'script';
        if (existing.prompt_template !== promptTemplate) updates.prompt_template = promptTemplate;
        if (existing.trigger_type !== triggerType) updates.trigger_type = triggerType;
        if (triggerConfig && existing.trigger_config !== triggerConfig) updates.trigger_config = triggerConfig;
        if (existing.auto_disabled_at) {
          updates.enabled = 1;
          updates.auto_disabled_at = null;
        }
        if (existing.auto_disabled_reason) updates.auto_disabled_reason = null;
        if (Object.keys(updates).length > 0) {
          brain.updateSkill(existing.id, updates);
          updated++;
          console.log(`[wall-e] Updated bundled skill: ${skill.name}`);
        }
      }
      continue;
    }

    brain.insertSkill({
      name: skill.name,
      description: skill.description || '',
      trigger_type: triggerType,
      trigger_config: triggerConfig,
      // Persist the legacy `INTERNAL_SKILL:` marker so downgrades can still
      // dispatch script skills via the prompt_template fallback. The schema
      // column `execution` is the authoritative source going forward.
      prompt_template: promptTemplate,
      execution: skill.execution === 'script' ? 'script' : 'agent',
    });
    added++;
    console.log(`[wall-e] Registered bundled skill: ${skill.name}`);
  }

  if (added > 0) {
    console.log(`[wall-e] Synced ${added} new bundled skill(s) to DB`);
  }
  if (updated > 0) {
    console.log(`[wall-e] Updated ${updated} bundled skill(s) in DB`);
  }
}

function bootstrapTasks() {
  const existing = brain.listTasks({ limit: 1 });
  if (existing.length > 0) return;
  const coreTasks = require('./core-tasks');
  for (const t of coreTasks) brain.insertTask(t);
  console.log('[wall-e] Bootstrapped ' + coreTasks.length + ' core tasks');
}

async function checkForUpdates() {
  try {
    const { execFile } = require('node:child_process');
    const { promisify } = require('node:util');
    const telemetry = require('./telemetry');
    const execFileAsync = promisify(execFile);
    const pkgPath = path.join(__dirname, '..', 'package.json');
    const current = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
    const { stdout } = await execFileAsync('npm', ['view', 'create-walle', 'version'], { timeout: 15000 });
    const latest = stdout.trim();
    // Only alert if published version is actually newer (not during local dev)
    const isNewer = (a, b) => {
      const pa = a.split('.').map(Number), pb = b.split('.').map(Number);
      for (let i = 0; i < 3; i++) { if ((pa[i] || 0) > (pb[i] || 0)) return true; if ((pa[i] || 0) < (pb[i] || 0)) return false; }
      return false;
    };
    const lag = (a, b) => {
      const latestParts = String(a || '').split('.').map(n => Number(n) || 0);
      const currentParts = String(b || '').split('.').map(n => Number(n) || 0);
      return {
        major: Math.max(0, (latestParts[0] || 0) - (currentParts[0] || 0)),
        minor: Math.max(0, (latestParts[1] || 0) - (currentParts[1] || 0)),
        patch: Math.max(0, (latestParts[2] || 0) - (currentParts[2] || 0)),
      };
    };
    if (latest && current && isNewer(latest, current)) {
      const versionLag = lag(latest, current);
      brain.setKv('update_available', JSON.stringify({
        current, latest, lag: versionLag, checked_at: new Date().toISOString(),
      }));
      telemetry.track('upgrade_check', { status: 'available', current, latest, lag: versionLag });
      telemetry.trackFunnelStep('upgrade_available');
      console.log(`[wall-e] Update available: ${current} -> ${latest}`);
    } else {
      brain.setKv('update_available', '');
      telemetry.track('upgrade_check', { status: 'current', current, latest: latest || 'unknown' });
    }
  } catch (err) {
    try {
      const telemetry = require('./telemetry');
      telemetry.track('upgrade_check', {
        status: 'failed',
        reason: err?.code || err?.name || 'error',
      });
    } catch {}
    // npm not available or network error — skip silently
  }
}

async function main() {
  console.log('[wall-e] Starting WALL-E agent daemon...');

  // Boot profiler — per-phase wall-clock timing so we can SEE where startup time goes
  // (logged below + surfaced at GET /api/wall-e/boot-profile). brain.initDb() records
  // its own initDb.* sub-phases into the same profile.
  const _bootp = require('./lib/boot-profile');
  const { performance: _perf } = require('perf_hooks');
  _bootp.reset();
  let _phaseLast = _perf.now();
  const _phase = (label) => { const n = _perf.now(); _bootp.mark(label, n - _phaseLast); _phaseLast = n; };

  const providerType = (process.env.WALLE_PROVIDER || 'anthropic').toLowerCase();
  console.log(`[wall-e] Provider: ${providerType}, Model: ${process.env.WALLE_MODEL || 'default'}`);

  const config = loadOrCreateConfig();
  _phase('loadConfig');

  // Start the event-loop delay monitor early so a synchronous DB query that wedges the
  // loop is surfaced in the logs (and on the health endpoint) instead of silently making
  // the daemon look alive while every HTTP request times out.
  try { require('./lib/event-loop-monitor').start(); } catch (e) { console.warn('[wall-e] event-loop monitor unavailable:', e.message); }

  // Init database (auto-creates data directory if needed)
  brain.initDb();
  _phase('brain.initDb');
  // backfillTemporalValidity is a ONE-TIME migration (new knowledge rows already set
  // valid_from on insert). On a large legacy brain it took ~16s on the boot path; it is
  // deferred off the readiness path below (after startServer) so it never blocks port bind.

  // Load API keys from brain DB into env if not already set
  // (setup page stores keys in DB; env var may not be present when spawned by CTM)
  const ENV_KEY_MAP = { anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY', google: 'GOOGLE_API_KEY' };
  const envKey = ENV_KEY_MAP[providerType];
  if (envKey && !process.env[envKey]) {
    try {
      const row = brain.getDb().prepare(
        `SELECT api_key_encrypted FROM model_providers
         WHERE type = ? AND enabled = 1 AND api_key_encrypted IS NOT NULL
         ORDER BY updated_at DESC LIMIT 1`
      ).get(providerType);
      if (row && row.api_key_encrypted) {
        process.env[envKey] = row.api_key_encrypted;
        console.log(`[wall-e] Loaded ${providerType} API key from brain DB`);
      }
    } catch (e) {
      console.warn(`[wall-e] Could not load API key from brain DB: ${e.message}`);
    }
  }

  if (providerType === 'anthropic' && !process.env.ANTHROPIC_API_KEY) {
    console.warn('[wall-e] WARNING: ANTHROPIC_API_KEY not set — think/reflect loops will fail.');
    console.warn('[wall-e] Copy .env.example to .env and add your API key.');
  } else if (providerType === 'openai' && !process.env.OPENAI_API_KEY) {
    console.warn('[wall-e] WARNING: OPENAI_API_KEY not set for OpenAI provider.');
  } else if (providerType === 'google' && !process.env.GOOGLE_API_KEY && !process.env.GEMINI_API_KEY) {
    console.warn('[wall-e] WARNING: GOOGLE_API_KEY not set for Google provider.');
  }
  _phase('loadApiKeys');
  brain.setDaemonOwned(); // Prevent bundled skills from closing the shared DB connection
  // NOTE: the daily backup is registered as a scheduler job below (name: 'daily-backup'),
  // NOT run here — it must never block boot / port bind / MCP readiness.

  // Embedding subsystem init is DEFERRED off the readiness path (see after startServer).
  // It's optional + degrades gracefully (boot-time memory inserts queue for the backfill
  // job to embed later), and on the live brain it cost ~15s — which must not block port
  // bind / MCP readiness.
  bootstrapSkills();
  syncBundledSkills();
  bootstrapTasks();
  _phase('bootstrap(skills+tasks)');

  // Item C (multi-agent deep-dive): ensure the default auth profile exists.
  // Idempotent — safe to call every boot. Existing skills with NULL
  // auth_profile_id continue to use globals; new skills can opt into the
  // 'default' profile or define their own (e.g. 'work-slack').
  try {
    const { ensureDefaultProfile } = require('./auth/profiles');
    ensureDefaultProfile();
  } catch (err) {
    console.warn('[wall-e] Auth profile setup failed (non-fatal):', err.message);
  }

  // Version check (async, non-blocking) — alert if a newer version is on npm
  checkForUpdates().catch(() => {});

  // Skill-reference sync (Learn+Reference system) does per-skill filesystem reads + a
  // skill_reference scan of the memories table — hundreds of ms to seconds on a large
  // brain. It is NOT needed for the daemon to serve requests, so it runs deferred off the
  // readiness path (after startServer) rather than blocking port bind. See below.
  _phase('skillRefSync+authProfile');

  // Set owner from config
  if (config.owner) {
    if (config.owner.name) brain.setOwner('name', String(config.owner.name).slice(0, 200));
    if (config.owner.first_name) brain.setOwner('first_name', String(config.owner.first_name).slice(0, 100));
    if (config.owner.last_name) brain.setOwner('last_name', String(config.owner.last_name).slice(0, 100));
    if (config.owner.timezone) brain.setOwner('timezone', String(config.owner.timezone).slice(0, 50));
  }
  console.log(`[wall-e] Owner: ${brain.getOwnerName()}`);

  // Build adapter list via the plugin registry. Live-loop adapters
  // declare mode=poll and an enabledByDefault flag; we walk them here
  // and instantiate the ones the config enables.
  adapterRegistry.ensureBootstrapped();
  const adapters = [];
  const adapterCtorOpts = {
    ctm: () => ({}),
    slack: () => ({
      ownerNames: [config.owner?.name, config.owner?.first_name].filter(Boolean),
    }),
  };

  for (const entry of adapterRegistry.list({ mode: 'poll' })) {
    if (!adapterRegistry.isPollEnabledByConfig(entry.manifest, config)) continue;
    const optsFn = adapterRegistry.has(entry.id) && adapterCtorOpts[entry.id];
    const inst = adapterRegistry.instantiate(entry.id, optsFn ? optsFn() : {});
    if (inst) {
      adapters.push(inst);
      console.log(`[wall-e] ${entry.manifest.label || entry.id} adapter enabled`);
    }
  }
  console.log(`[wall-e] ${adapters.length} adapter(s) active`);
  _phase('adapters');

  // Initialize messaging channels via the plugin registry.
  // Each channel manifest declares its configKey + required config paths;
  // we walk the registered channels, check enablement, and instantiate.
  channelRegistry.ensureBootstrapped();
  const channels = [];
  const channelCtorOpts = {
    imessage: () => ({
      buddyId: config.channels.imessage.buddy_id,
      onMessage: async (text /* , sender */) => {
        const result = await walleChat(text, { channel: 'imessage' });
        return result.reply;
      },
    }),
    slack_dm: () => ({
      botToken: config.channels.slack_dm.bot_token,
      onMessage: async (text /* , sender */) => {
        const result = await walleChat(text, { channel: 'slack_dm' });
        return result.reply;
      },
    }),
    telegram: () => ({
      botToken: config.channels.telegram.bot_token,
      ownerChatId: config.channels.telegram.owner_chat_id,
      onMessage: async (text /* , sender */) => {
        const result = await walleChat(text, { channel: 'telegram' });
        return result.reply;
      },
    }),
  };

  for (const entry of channelRegistry.list()) {
    if (!channelRegistry.isEnabledByConfig(entry.manifest, config)) continue;
    if (!channelRegistry.configRequirementsSatisfied(entry.manifest, config)) {
      console.warn(`[wall-e] Channel "${entry.id}" enabled but missing required config — skipping`);
      continue;
    }
    const optsFn = channelCtorOpts[entry.id];
    if (!optsFn) {
      console.warn(`[wall-e] Channel "${entry.id}" registered but no constructor opts builder — skipping (3rd-party plugins should register their own bootstrap)`);
      continue;
    }
    const ch = channelRegistry.instantiate(entry.id, optsFn());
    if (ch) {
      channels.push(ch);
      console.log(`[wall-e] ${entry.manifest.label || entry.id} channel enabled`);
    }
  }

  // Start all channels
  for (const ch of channels) {
    ch.start().catch(err => console.error(`[wall-e] Channel ${ch.name} start failed:`, err.message));
  }
  if (channels.length > 0) {
    console.log(`[wall-e] ${channels.length} channel(s) started`);
  }
  _phase('channels');

  // Always start HTTP server — WALL-E API should be independently reachable
  const { startServer } = require('./server');
  let httpServer;
  try {
    const _serverT0 = _perf.now();
    httpServer = startServer();
    if (httpServer && httpServer.ready) await httpServer.ready;
    _bootp.mark('startServer(portBind)', _perf.now() - _serverT0);
    _phaseLast = _perf.now();
    _bootp.done();
    console.log(_bootp.summary());
  } catch (e) {
    console.error('[wall-e] HTTP server failed to start:', e.message);
    process.exit(e && e.code === 'EADDRINUSE' ? 98 : 1);
  }

  // Deferred, off the readiness path: heavy/optional init that must not delay port bind +
  // MCP readiness. Runs a few seconds after the server is up (the readiness probe completes
  // first). unref'd so it never holds the process. Sub-phase timings are still captured in
  // the boot profile (the marks append after startServer), so /api/wall-e/boot-profile shows
  // exactly which embeddings sub-step is slow on the live brain.
  setTimeout(() => {
    // 1) Embedding subsystem (optional; degrades gracefully — inserts queue for the backfill job)
    try {
      require('./embeddings').init();
    } catch (e) {
      console.log('[wall-e] Embeddings not available:', e.message);
    }
    // 2) One-time temporal-validity backfill (no-op on an already-migrated brain)
    try {
      const n = brain.backfillTemporalValidity();
      if (n > 0) console.log(`[wall-e] Temporal-validity backfill completed (${n} rows, deferred off boot)`);
    } catch (err) {
      console.warn('[wall-e] Deferred temporal-validity backfill failed (non-fatal):', err.message);
    }
    // 3) Skill-reference sync — deferred off the readiness path (was on the boot path,
    //    where its filesystem reads + skill_reference scan delayed HTTP port bind).
    try {
      const { syncSkillReferences, checkImportedSkillUpdates } = require('./skills/claude-code-reader');
      const refResult = syncSkillReferences();
      if (refResult.synced > 0) console.log(`[wall-e] Synced ${refResult.synced} skill references to brain (deferred off boot)`);
      const updates = checkImportedSkillUpdates();
      if (updates.length > 0) console.log(`[wall-e] Imported skill updates: ${updates.map(u => u.name + '(' + u.action + ')').join(', ')}`);
    } catch (e) {
      console.warn('[wall-e] Skill reference sync error:', e.message);
    }
  }, 2000).unref?.();

  // Slack Socket Mode is optional and event-driven. It only starts when an
  // app-level xapp token is configured, so the existing polling path remains
  // unchanged for workspaces that have not registered a Wall-E Slack app.
  let slackSocketMode = null;
  try {
    const { startSlackSocketModeListener } = require('./slack/socket-mode-listener');
    slackSocketMode = startSlackSocketModeListener();
  } catch (e) {
    console.warn('[wall-e] Slack Socket Mode startup skipped:', e.message);
  }

  // Start telemetry (anonymous, opt-out via WALLE_TELEMETRY=0)
  telemetry.printNoticeIfFirstRun();
  telemetry.start();
  telemetry.track('boot', {
    channels: channels.map(c => c.name),
    adapters: adapters.map(a => a.constructor?.name || 'unknown'),
  });
  try { require('./embeddings').trackEmbeddingStatus('boot'); } catch {}
  telemetry.trackFunnelStep('boot');

  // Recover tasks that were interrupted by previous shutdown
  const recovered = recoverInterruptedTasks();
  if (recovered > 0) console.log(`[wall-e] Recovered ${recovered} interrupted task(s)`);

  // Initial ingest (wrapped — must not crash the daemon)
  let ingestResult = { memoriesIngested: 0 };
  try {
    console.log('[wall-e] Running initial ingest...');
    ingestResult = await ingest.runOnce(adapters);
    console.log(`[wall-e] Initial ingest: ${ingestResult.memoriesIngested} memories`);
    if (ingestResult.memoriesIngested > 0) {
      telemetry.track('ingest', { memories: ingestResult.memoriesIngested, initial: true });
      telemetry.trackFunnelStep('first_ingest');
    }
  } catch (e) {
    console.error('[wall-e] Initial ingest failed (non-fatal):', e.message);
  }

  // Initial think (wrapped — must not crash the daemon)
  if (ingestResult.memoriesIngested > 0) {
    try {
      console.log('[wall-e] Running initial think...');
      const thinkResult = await think.runOnce();
      console.log(`[wall-e] Initial think: ${thinkResult.memoriesProcessed} processed, ${thinkResult.knowledgeExtracted} knowledge`);
    } catch (e) {
      console.error('[wall-e] Initial think failed (non-fatal):', e.message);
    }
  }

  // --- Central Scheduler ---
  const scheduler = new Scheduler({
    tickMs: 5000,
    pools: { llm: 2, ollama: 1, embedding: 1, io: 3 },
    shutdownTimeoutMs: 5000,
    // Failure-alert: after 2 consecutive errors on a single job (with 1h
    // cooldown), write a Service Alert that the CTM Wall-E tab surfaces.
    // Borrowed from OpenClaw's resolveFailureAlert + emitFailureAlert
    // (src/cron/service/timer.ts:300-465).
    failureAlert: true,
    failureAlertBrain: brain,
    // Persistent job state — every completion writes to scheduler_job_state
    // and register() hydrates from there. Lets runMissedJobs() replay
    // anything whose nextEligibleAt fell while the daemon was down.
    // Borrowed from OpenClaw's cron store (src/cron/service/store.ts +
    // runMissedJobs in timer.ts:962-1040).
    persistJobState: true,
    persistBrain: brain,
  });
  Scheduler.setInstance(scheduler);
  scheduler.on('tickError', (err) => {
    _writeCrashLog('SCHEDULER TICK ERROR', err);
    telemetry.trackError('scheduler', err);
  });

  const { getShadowConfig } = require('./eval/shadow');
  let replayCycle = 0;
  const LEARNER_INTERVAL_MS = 3600000;

  // Boot-time stagger: registered jobs share pools and dependencies. Without
  // explicit startDelayMs, every job fires on the first tick (5s after boot)
  // and races for the same pool slots. The values below spread the unstaggered
  // jobs across the first 35s of boot so each pool drains in priority order
  // without contention spikes. Staggers already specified on training/replay/
  // reflect/harvest/dedup are preserved.
  scheduler.register({
    name: 'ingest',
    run: () => ingest.runOnce(adapters),
    intervalMs: clampInterval(config.intervals?.ingest_ms, 60000),
    pool: 'io',
    priority: 1,
    startDelayMs: 2000,
    onResult: (r) => {
      if (r.memoriesIngested > 0) console.log(`[wall-e] Ingested ${r.memoriesIngested} memories`);
    },
    onError: (err) => {
      console.error('[wall-e] Ingest error:', err.message);
      telemetry.trackError('ingest', err);
    },
  });

  scheduler.register({
    name: 'think',
    run: () => think.runOnce(),
    intervalMs: clampInterval(config.intervals?.think_ms, 120000),
    pool: 'llm',
    priority: 2,
    startDelayMs: 5000,
    dependsOn: ['ingest'],
    cascadeFrom: ['ingest'],
    cascadeCheck: (r) => r.memoriesIngested > 0,
    onResult: (r) => {
      if (r.knowledgeExtracted > 0) console.log(`[wall-e] Extracted ${r.knowledgeExtracted} knowledge entries`);
      telemetry.track('think', { processed: r.memoriesProcessed, knowledge: r.knowledgeExtracted });
    },
    onError: (err) => {
      console.error('[wall-e] Think error:', err.message);
      telemetry.trackError('think', err);
    },
  });

  scheduler.register({
    name: 'tasks',
    run: () => runDueTasks(),
    intervalMs: clampInterval(config.intervals?.tasks_ms, 30000),
    pool: 'llm',
    priority: 3,
    startDelayMs: 8000,
    onResult: (r) => {
      if (r.processed > 0) {
        console.log(`[wall-e] Tasks: ${r.processed} completed`);
        telemetry.track('task_run', { processed: r.processed });
      }
    },
    onError: (err) => {
      console.error('[wall-e] Tasks error:', err.message);
      telemetry.trackError('tasks', err);
    },
  });

  scheduler.register({
    name: 'skills',
    // Item A of provider-routing deep-dive: thread per-job model
    // override into runDueSkills. The scheduler injects modelOverride
    // from kv_store[scheduler_model_overrides:<name>]; runDueSkills
    // forwards to runSkillWithFallback's opts.model_candidates.
    run: ({ modelOverride } = {}) => runDueSkills({ modelOverride }),
    intervalMs: clampInterval(config.intervals?.skills_ms, 300000),
    pool: 'llm',
    priority: 4,
    startDelayMs: 11000,
    onResult: (r) => {
      if (r.executed > 0) {
        console.log(`[wall-e] Skills: ${r.executed} executed, ${r.memoriesCreated} memories`);
        telemetry.track('skill_run', { executed: r.executed, memories: r.memoriesCreated });
        telemetry.trackFunnelStep('first_skill');
      }
    },
    onError: (err) => {
      console.error('[wall-e] Skills error:', err.message);
      telemetry.trackError('skills', err);
    },
  });

  scheduler.register({
    name: 'training',
    run: async () => {
      const { runBatchEval } = require('./eval/evaluator');
      const { updateAllModelTaskScores } = require('./eval/aggregator');
      const { checkPromotions } = require('./eval/promoter');

      const evalResult = await runBatchEval(brain, { limit: 50 });
      if (evalResult.evaluated > 0) {
        console.log(`[eval] Evaluated ${evalResult.evaluated} shadow results, avg score: ${evalResult.avgScore}`);
      }

      const aggResult = updateAllModelTaskScores(brain);
      if (aggResult.updated > 0) {
        console.log(`[eval] Aggregated scores for ${aggResult.updated} model/task pairs`);
      }

      const promoResult = checkPromotions(brain);
      if (promoResult.alertsCreated > 0) {
        console.log(`[eval] Created ${promoResult.alertsCreated} promotion alerts`);
      }

      if (Date.now() - lastLearnerRun >= LEARNER_INTERVAL_MS) {
        const { updateModelPreferences } = require('./evaluation/learner');
        const learnResult = updateModelPreferences(brain);
        if (learnResult.updates?.length > 0) {
          console.log(`[eval] Auto-updated ${learnResult.updates.length} model defaults`);
        }
        lastLearnerRun = Date.now();
      }
      return evalResult;
    },
    intervalMs: clampInterval(config.intervals?.training_ms, 300000),
    pool: 'ollama',
    priority: 5,
    startDelayMs: 20000,
    shouldRun: () => getShadowConfig().enabled,
    onError: (err) => console.error('[eval] Loop error:', err.message),
  });

  scheduler.register({
    name: 'replay',
    run: async () => {
      const { runReplay, runMultiTurnReplay } = require('./eval/replay');
      let result;
      if (replayCycle % 2 === 0) {
        result = await runReplay(brain, { limit: 50 });
        if (result.replayed > 0) console.log(`[replay] Replayed ${result.replayed} single-turn prompts through shadow`);
      } else {
        result = await runMultiTurnReplay(brain, { sessionLimit: 5, turnsPerSession: 5 });
        if (result.replayed > 0) console.log(`[replay-mt] Replayed ${result.replayed} turns across ${result.sessions} multi-turn sessions`);
      }
      replayCycle++;
      return result;
    },
    intervalMs: clampInterval(config.intervals?.replay_ms, 300000),
    pool: 'ollama',
    priority: 5,
    startDelayMs: 15000,
    shouldRun: () => getShadowConfig().enabled,
    onError: (err) => console.error('[replay] Loop error:', err.message),
  });

  scheduler.register({
    name: 'initiative',
    run: ({ trigger }) => {
      const { runInitiativeLoop } = require('./loops/initiative');
      return runInitiativeLoop({ trigger });
    },
    intervalMs: clampInterval(config.intervals?.initiative_ms, 300000),
    pool: 'llm',
    priority: 6,
    startDelayMs: 25000,
    debounceMs: 30000,
    onResult: (r) => {
      if (r?.skipped) return;
      if (r.decision !== 'noop' && r.decision !== 'observed') {
        console.log(`[wall-e] Initiative: ${r.decision} — ${r.reasoning?.slice(0, 100)}`);
      }
    },
    onError: (err) => {
      console.error('[wall-e] Initiative error:', err.message);
      telemetry.trackError('initiative', err);
    },
  });

  scheduler.register({
    name: 'reflect',
    run: () => reflect.runOnce(),
    intervalMs: clampInterval(config.intervals?.reflect_ms, 3600000),
    pool: 'llm',
    priority: 7,
    dependsOn: ['think'],
    startDelayMs: 30000,
    onResult: (r) => {
      if (r.summaryGenerated) console.log('[wall-e] Daily summary generated');
      telemetry.track('reflect', { summary: r.summaryGenerated ? 1 : 0 });
    },
    onError: (err) => {
      console.error('[wall-e] Reflect error:', err.message);
      telemetry.trackError('reflect', err);
    },
  });

  scheduler.register({
    name: 'harvest',
    run: async () => {
      const { runHarvest } = require('./eval/harvester');
      const result = await runHarvest({ brain });
      if (result.stats.total > 0) {
        console.log(`[harvest] Collected ${result.stats.total} samples from ${Object.keys(result.stats.bySource).join(', ')}`);
      }
      return result;
    },
    intervalMs: 600000,
    pool: 'io',
    priority: 8,
    startDelayMs: 30000,
    onError: (err) => console.error('[harvest] Loop error:', err.message),
  });

  scheduler.register({
    name: 'backfill',
    run: async () => {
      const backfill = require('./loops/backfill');
      const result = await backfill.runOnce();
      const parts = [];
      if (result.classified > 0) parts.push(`${result.classified} classified`);
      if (result.embedded > 0) parts.push(`${result.embedded} embedded`);
      if (result.heuristic > 0) parts.push(`${result.heuristic} heuristic knowledge`);
      if (result.entityLinked > 0) parts.push(`${result.entityLinked} entity-linked`);
      if (result.indexed > 0) parts.push(`${result.indexed} indexed`);
      if (result.noiseStripped > 0) parts.push(`${result.noiseStripped} noise-stripped`);
      if (parts.length > 0) console.log(`[wall-e] Backfill: ${parts.join(', ')}`);
      if (result.done) return { ...result, _done: true };
      return result;
    },
    intervalMs: 30000,
    pool: 'embedding',
    priority: 9,
    startDelayMs: 35000,
    timeoutMs: 90000,
    onError: (err) => {
      console.error('[wall-e] Backfill error:', err.message);
      telemetry.trackError('backfill', err);
    },
  });

  scheduler.register({
    name: 'brain-optimize',
    run: async () => require('./loops/brain-optimize').runOnce(),
    intervalMs: 3600000,    // hourly; self-gates to a one-shot via kv (brain:optimize:v1)
    pool: 'io',
    priority: 4,
    startDelayMs: 180000,   // 3 min after boot — well past the boot storm
    timeoutMs: 120000,
    onError: (err) => console.error('[brain-optimize] Loop error:', err.message),
  });

  scheduler.register({
    name: 'dedup',
    run: async () => {
      const dedup = require('./utils/dedup');
      return dedup.runOnce({ threshold: 0.85, limit: 500, dryRun: false });
    },
    intervalMs: 24 * 60 * 60 * 1000, // daily
    pool: 'embedding',
    priority: 10,
    startDelayMs: 120000, // wait 2min after startup
    onResult: (r) => {
      if (r.archived > 0) console.log(`[wall-e] Dedup: archived ${r.archived} duplicates`);
    },
    onError: (err) => console.error('[dedup] Error:', err.message),
  });

  // Daily brain backup. Registered as a scheduler job (NOT run at boot) so the heavy
  // backup never blocks port bind / MCP readiness; the scheduler owns "once a day +
  // replay if a day was missed during downtime" (persistJobState). ensureDailyBackup()
  // is idempotent (skips if today's already exists) and createBackup is now non-blocking.
  scheduler.register({
    name: 'daily-backup',
    run: async () => {
      const r = await brain.ensureDailyBackup();
      if (r && r.created) console.log(`[wall-e] Daily backup: ${r.backupName}`);
      return r;
    },
    intervalMs: 24 * 60 * 60 * 1000, // daily
    pool: 'io',
    priority: 11,
    startDelayMs: 180000, // 3min after startup — well off the boot/readiness path
    onError: (err) => {
      console.error('[wall-e] Daily backup error:', err.message);
      telemetry.trackError('daily-backup', err);
    },
  });

  // Brain retention: prune unbounded tables + orphan embeddings. Hourly check; each prune
  // is internally age-gated + batched (lock-friendly). See the 2026-05-31 audit.
  scheduler.register({
    name: 'brain-retention',
    run: async () => brain.runBrainRetention(),
    intervalMs: 60 * 60 * 1000,
    pool: 'io',
    priority: 11,
    startDelayMs: 90000,
    onResult: (r) => {
      const nonZero = Object.entries(r || {}).filter(([, v]) => typeof v === 'number' && v > 0);
      if (nonZero.length) console.log('[wall-e] Retention pruned: ' + nonZero.map(([k, v]) => `${k}=${v}`).join(', '));
    },
    onError: (err) => console.error('[brain-retention] Error:', err.message),
  });

  // Daily question digest: push the day's questions to opt-in channels (Slack/email). The
  // CTM Wall-E tab always shows pending questions, so it needs no push. runDigestJob is
  // self-gated to once/day at/after WALL_E_DIGEST_HOUR; we just tick it hourly.
  scheduler.register({
    name: 'question-digest',
    run: async () => {
      const { runDigestJob } = require('./loops/question-digest');
      return runDigestJob();
    },
    intervalMs: 60 * 60 * 1000,
    pool: 'io',
    priority: 11,
    startDelayMs: 100000,
    onResult: (r) => {
      if (r && r.sent) console.log(`[wall-e] Question digest sent (${r.count}): ${JSON.stringify(r.results)}`);
    },
    onError: (err) => console.error('[question-digest] Error:', err.message),
  });

  // Listening: Calendar sync (every 5 minutes)
  scheduler.register({
    name: 'listening:calendar-sync',
    run: async () => {
      const calendar = require('./listening/calendar');
      return calendar.syncUpcomingMeetings(2);
    },
    intervalMs: 300000,
    pool: 'io',
    priority: 7,
    startDelayMs: 12000,
    shouldRun: () => {
      try {
        const calendar = require('./listening/calendar');
        return !!calendar.getCalendarSource();
      } catch { return false; }
    },
    onResult: (r) => {
      if (r.synced > 0) console.log(`[wall-e] Calendar synced ${r.synced} meetings`);
    },
    onError: (err) => console.error('[listening] Calendar sync error:', err.message),
  });

  // Listening: Recording cleanup (daily)
  scheduler.register({
    name: 'listening:cleanup',
    run: async () => {
      const storage = require('./listening/storage');
      const cleaned = storage.cleanupOldRecordings(90);
      return { cleaned };
    },
    intervalMs: 24 * 60 * 60 * 1000,
    pool: 'io',
    priority: 10,
    startDelayMs: 300000,
    onResult: (r) => {
      if (r.cleaned > 0) console.log(`[wall-e] Cleaned ${r.cleaned} old recordings`);
    },
    onError: (err) => console.error('[listening] Cleanup error:', err.message),
  });

  // Telemetry cleanup (daily). Destructive cleanup is opt-in via
  // WALLE_TELEMETRY_CLEANUP_ENABLED=1; otherwise the scheduled job records a
  // dry-run so operators can inspect candidate counts before enabling it.
  scheduler.register({
    name: 'telemetry:cleanup',
    run: async () => {
      const { runTelemetryCleanup } = require('./api-walle');
      const live = process.env.WALLE_TELEMETRY_CLEANUP_ENABLED === '1';
      return runTelemetryCleanup({
        dryRun: !live,
        confirm: live ? 'delete' : '',
        source: 'scheduler',
      });
    },
    intervalMs: 24 * 60 * 60 * 1000,
    pool: 'io',
    priority: 10,
    startDelayMs: 295000,
    shouldRun: () => process.env.WALLE_TELEMETRY_SERVER === '1',
    onResult: (r) => {
      const changed = r?.changed || {};
      const deleted = changed.raw_events_deleted || 0;
      const redacted = changed.feedback_reports_redacted || 0;
      if (r?.dry_run) {
        const candidates = r?.candidates?.raw_events_delete || 0;
        if (candidates > 0) console.log(`[telemetry] Cleanup dry-run found ${candidates} raw event candidate(s)`);
      } else if (deleted > 0 || redacted > 0) {
        console.log(`[telemetry] Cleanup deleted ${deleted} raw event(s), redacted ${redacted} feedback report(s)`);
      }
    },
    onError: (err) => console.error('[telemetry] Cleanup error:', err.message),
  });

  // Weekly coding-agent benchmark suite (Phase 5)
  scheduler.register({
    name: 'agent-benchmark',
    run: async () => {
      const { runAgentBenchmarkSuite, detectRegressions, resolveModelName } = require('./eval/agent-runner');
      const { runAgentLoop } = require('./coding-orchestrator');

      const result = await runAgentBenchmarkSuite({
        brain,
        runAgentLoop,
        model: resolveModelName(),
        timeoutMs: 120000,
      });

      if (result.completed > 0) {
        console.log(`[agent-bench] Ran ${result.completed}/${result.totalBenchmarks} benchmarks, avg score: ${result.avgScore.toFixed(3)}`);

        const regressions = detectRegressions(brain, result.results);
        if (regressions.length > 0) {
          console.warn(`[agent-bench] ${regressions.length} regressions detected:`, regressions.map(r => `${r.benchmarkId}: ${r.dropPct}% drop`).join(', '));
        }
      }

      return result;
    },
    intervalMs: 7 * 24 * 60 * 60 * 1000, // Weekly
    pool: 'llm',
    priority: 10,
    startDelayMs: 300000, // 5min delay on startup
    shouldRun: () => {
      // Only run if we have coding-agent benchmarks
      try {
        require('./eval/benchmarks/coding-agent.json');
        return true;
      } catch { return false; }
    },
    onError: (err) => console.error('[agent-bench] Error:', err.message),
  });

  // Weekly full evaluation loop (Phase 8: benchmarks + regressions + mining + promotions + aggregation)
  scheduler.register({
    name: 'weekly-eval-loop',
    run: async () => {
      const { run: runWeeklyEval } = require('./eval/weekly-eval-loop');
      const { runAgentLoop } = require('./coding-orchestrator');
      const summary = await runWeeklyEval(brain, { runAgentLoop });
      const errors = summary.steps.filter(s => s.status === 'error');
      if (errors.length > 0) {
        console.warn(`[weekly-eval] ${errors.length} steps had errors:`, errors.map(e => `${e.name}: ${e.detail}`).join('; '));
      }
      console.log(`[weekly-eval] Completed: ${summary.steps.filter(s => s.status === 'completed').length}/${summary.steps.length} steps`);
      if (summary.regressions.length > 0) {
        console.warn(`[weekly-eval] ${summary.regressions.length} regressions detected`);
      }
      return summary;
    },
    intervalMs: 7 * 24 * 60 * 60 * 1000, // Weekly
    pool: 'llm',
    priority: 5,
    startDelayMs: 600000, // 10min delay on startup
    shouldRun: () => {
      // Run on Sundays only
      return new Date().getDay() === 0;
    },
    onError: (err) => console.error('[weekly-eval] Error:', err.message),
  });

  // Note: session mining runs inside weekly-eval-loop (step 3), no separate scheduler needed.

  // Initial backfill integrity check
  try {
    const backfill = require('./loops/backfill');
    backfill.checkIntegrity();
    const progress = backfill.getProgress();
    if (!progress.done) {
      console.log(`[wall-e] Backfill pending: ${progress.unclassified} unclassified, ${progress.unembedded} unembedded`);
    }
  } catch {}

  // Event-driven initiative wake
  const eventBus = require('./events/event-bus');
  eventBus.on('message', (evt) => scheduler.wake('initiative', `message from ${evt.channel}`));
  eventBus.on('high_importance', () => scheduler.wake('initiative', 'high importance memory'));
  eventBus.on('webhook', (evt) => scheduler.wake('initiative', `webhook from ${evt.source}`));

  // Boot phase — read ~/.walle/BOOT.md and fire ONE LLM turn that follows
  // its instructions. Best-effort; never crashes the daemon. Borrowed from
  // OpenClaw's gateway/boot.ts. Runs BEFORE scheduler.start() so the user's
  // contract is honored before the recurring loops fire.
  try {
    const { runBootCheck } = require('./loops/boot');
    const bootResult = await runBootCheck();
    if (bootResult.status === 'ran') {
      console.log(`[wall-e] BOOT.md ran (latency ${bootResult.latencyMs}ms, ack=${bootResult.acknowledged})`);
      telemetry.track('boot_check', { status: 'ran', latency_ms: bootResult.latencyMs, ack: bootResult.acknowledged ? 1 : 0 });
    } else if (bootResult.status === 'missing') {
      // No BOOT.md — silent, this is the default state.
      telemetry.track('boot_check', { status: 'missing' });
    } else if (bootResult.status === 'empty') {
      console.log(`[wall-e] BOOT.md is empty (${bootResult.file_status}) — skipping boot LLM call`);
      telemetry.track('boot_check', { status: bootResult.file_status });
    } else {
      console.warn(`[wall-e] BOOT.md run failed: ${bootResult.reason}${bootResult.error ? ' — ' + bootResult.error : ''}`);
      telemetry.track('boot_check', { status: 'failed', reason: bootResult.reason });
    }
  } catch (e) {
    console.error('[wall-e] BOOT.md run threw (non-fatal):', e.message);
    telemetry.trackError('boot_check', e);
  }

  // Restart catch-up — replay jobs whose persisted nextEligibleAt fell
  // while the daemon was down. Cap of 5 immediate runs (5s stagger);
  // anything beyond that is deferred to the periodic tick. Borrowed
  // from OpenClaw's runMissedJobs (src/cron/service/timer.ts:962-1040).
  try {
    const catchup = await scheduler.runMissedJobs();
    if (catchup.ran > 0 || catchup.deferred > 0) {
      console.log(
        `[wall-e] Restart catch-up: ran ${catchup.ran}, deferred ${catchup.deferred}`
      );
      telemetry.track('restart_catchup', {
        ran: catchup.ran,
        deferred: catchup.deferred,
      });
    }
  } catch (e) {
    console.warn('[wall-e] runMissedJobs threw (non-fatal):', e.message);
  }

  // Start the scheduler
  scheduler.start();

  // Hot-reload config on file changes
  const { ConfigWatcher } = require('./lib/config-watcher');
  const configWatcher = new ConfigWatcher();
  configWatcher.start();

  configWatcher.on('intervals', (intervals) => {
    const updates = {
      ingest: intervals.ingest_ms,
      think: intervals.think_ms,
      tasks: intervals.tasks_ms,
      skills: intervals.skills_ms,
      training: intervals.training_ms,
      replay: intervals.replay_ms,
      initiative: intervals.initiative_ms,
      reflect: intervals.reflect_ms,
    };
    for (const [name, ms] of Object.entries(updates)) {
      if (ms && scheduler.hasJob(name)) {
        scheduler.updateInterval(name, clampInterval(ms, 60000));
      }
    }
    console.log('[wall-e] Intervals updated from config');
  });

  const jobNames = [...scheduler._jobs.keys()];
  console.log(`[wall-e] Scheduler started with ${jobNames.length} jobs: ${jobNames.join(', ')}`);
  console.log('[wall-e] Press Ctrl+C to stop.');

  // Graceful shutdown
  const shutdown = async () => {
    if (shuttingDown) return;
    shuttingDown = true;
    console.log('\n[wall-e] Shutting down...');

    // Hard deadline: exit no matter what after 5s (prevents SIGTERM hang)
    const forceTimer = setTimeout(() => {
      console.error('[wall-e] Shutdown timed out after 5s — force exiting');
      process.exit(0); // exit 0: intentional shutdown, not a crash (prevents CTM crash loop counter)
    }, 5000);
    forceTimer.unref(); // don't keep process alive for the timer itself

    try {
      await scheduler.shutdown();
      configWatcher.stop();
      eventBus.removeAllListeners();

      for (const ch of channels) {
        try { await ch.stop(); } catch {}
      }
      if (slackSocketMode) {
        try { slackSocketMode.stop(); } catch {}
      }

      telemetry.track('shutdown', { uptime: process.uptime() });
      telemetry.stop();
      try { require('./skills/mcp-client').disconnectAll(); } catch {}
      try {
        const drained = await brain.drainOwnerWrites({ timeoutMs: 1500 });
        if (drained?.timedOut) {
          console.error(`[wall-e] Timed out waiting for brain write queue; active=${drained.active || 'none'} pending=${drained.pending || 0}`);
        }
      } catch {}
      if (httpServer) httpServer.close();
      brain.closeDb(true); // force=true bypasses daemon guard
    } catch (e) {
      console.error('[wall-e] Error during shutdown:', e.message);
    }
    process.exit(0);
  };
  process.on('SIGINT', shutdown);
  process.on('SIGTERM', shutdown);
}

main().catch(err => {
  console.error('[wall-e] Fatal error in main():', err);
  _writeCrashLog('FATAL', err);
  // DO NOT process.exit(1) — keep the process alive.
  // The HTTP server and scheduler may still be partially functional.
  // CTM's crash loop detector will kill us if we keep crashing.
  // If the error is truly unrecoverable, the process will idle harmlessly
  // until CTM restarts it (which clears the transient condition).
  console.error('[wall-e] Keeping process alive despite fatal error — HTTP server may still be reachable.');
});
