// src/core/MatrixActor.ts
//
// MatrixActor — Unified headless actor class.
//
// Merges MatrixComponent + MatrixComposite into a single class:
// - Every actor CAN have children (lazy-initialized — zero overhead if unused)
// - System commands ($join, $leave, introspect, $ping) auto-subscribed
// - Domain commands opt-in via static accepts
// - Observable state via setState()
// - Cross-root invocation via invoke()
// - Deep template bindings via _wireDeepBindings()
//
// In the actor model (Erlang/OTP), every process can supervise children.
// There is no leaf/composite distinction. This class reflects that truth.

import type { IMatrixContext } from '../engine/core/IMatrixContext';
import type { IComponentDescriptor } from '../engine/core/IComponentDescriptor';
import { TopicRouter } from '../engine/messaging/TopicRouter';
import { SubscriptionBag } from '../engine/messaging/SubscriptionBag';
import type { MxEnvelope } from '../engine/messaging/MxEnvelope';
import { toHandlerCase } from '../engine/utils/NameUtils';
import { RequestReplyHelper } from './messaging/RequestReplyHelper';
import { RequestReply } from '../engine/remoting/RequestReply';
import { ActorBehavior } from './actor/ActorBehavior';
import { LispDispatchStrategy } from './lisp/LispDispatchStrategy';
import type { IMailbox, IExitSignal, IMonitorRef } from '../engine/mailbox/IMailbox';
import { LamportClock } from '../engine/messaging/LamportClock';
import { ChildManager } from './children/ChildManager';
import {
  type ISupervisionPolicy,
  DEFAULT_SUPERVISION_POLICY,
} from './Supervision';
import {
  SupervisorBehavior,
  type ISupervisorPolicy,
} from './actor/SupervisorBehavior';
import {
  SecurityRealm,
  DEFAULT_SECURITY_POLICY,
  type ISupervisorSecurityPolicy,
} from './security';
import type { ICapabilityToken } from './security/ICapabilityToken';
import { MembraneStepper, DEFAULT_MEMBRANE_SOURCE, type MembraneResult } from './membrane/index.js';
import type { IPolicyEngine, IPolicyDecision } from './security/IPolicyEngine';
import type { IRecordStore } from '../framework/IRecordStore';
import { RECORD_STORE_KEY } from '../framework/InteractionRecord';
import type { IInterfaceDescriptor } from '../engine/reflection/IInterfaceDescriptor';
import type {
  IComponentClassWithTemplate,
  ComponentFactory,
  ICompositeIntrospectChild,
  IComponentTreeDescriptor,
  IChildDescriptorEntry,
  Position,
  IAddChildOptions,
  IDynamicComponentDefinition,
} from './composite';
import {
  COMPONENT_RUNTIME_METADATA,
  type IComponentRuntimeMetadata,
} from './composite/IComponentRuntimeMetadata';
import type { HealthSnapshotV1 } from '@open-matrix/contracts/service-registry';

// ═══════════════════════════════════════════════════════════════════════════
// Public types
// ═══════════════════════════════════════════════════════════════════════════

export type MessageHandler = (payload: unknown, meta: { topic: string; ts: number }) => void | Promise<void>;

export type ComponentHandler = (payload: unknown, meta: { topic: string; ts: number }) => unknown | Promise<unknown>;

export interface ActorStatusSnapshotV1 {
  mount: string;
  kind: string;
  uptime: number;
  lastMessageAt: number | null;
  runtimeId: string | null;
  packageId: string | null;
}

type TemplateComponentClass = IComponentClassWithTemplate & {
  template?: string;
};

function isMergeablePayload(value: unknown): value is Record<string, unknown> {
  return value != null && typeof value === 'object' && !Array.isArray(value);
}

function isClosedTransportPublishError(error: unknown): boolean {
  const code = typeof (error as { code?: unknown })?.code === 'string'
    ? String((error as { code?: unknown }).code)
    : '';
  const message = error instanceof Error ? error.message : String(error ?? '');
  return code === 'CONNECTION_CLOSED' || /connection[_\s-]*closed/i.test(message);
}

/**
 * Deep binding descriptor — produced by the DSL compiler.
 * Maps a descendant event to a handler on the template author's actor.
 */
export interface DeepBindingDescriptor {
  /** Dot-delimited descendant path relative to this actor, e.g., 'split.preview.syntax' */
  relativePath: string;
  /** Event name, e.g., 'HighlightReady' */
  event: string;
  /** Handler method name on this actor, e.g., 'onHighlightReadyFromSyntax' */
  handler: string;
}

// Re-export Position type for convenience
export type { Position } from './composite';

// ═══════════════════════════════════════════════════════════════════════════
// System commands — auto-subscribed, NOT in static accepts
// ═══════════════════════════════════════════════════════════════════════════

/**
 * System commands that every actor subscribes to automatically.
 * These are NOT declared in static accepts — they're infrastructure.
 */
const SYSTEM_COMMANDS = [
  '$join',
  '$join_ack',
  '$leave',
  'introspect',
  '$introspect',
  '$ping',
  '$status',
  '$health',
  '$reply',
  '$getState',
  '$getHistory',
  '$createControl',
  '$activity',
  '$preferences',
  '$config',
  '$membrane-inherit',
  '$membrane',
  '$skill',
  '$cognitive.set',
  '$cognitive.get',
  '$triggers.add',
  '$triggers.remove',
  '$triggers.list',
  '$skills.add',
  '$skills.remove',
  // NOTE: $prompt and $source-code are intentionally NOT here.
  // They live in headlessFrameworkOps (see _subscribeToInbox) so that
  // shell-managed actors defer to the shell handler, avoiding the
  // dual-dispatch race (actor + shell both fire for the same op).
] as const;

const SANITIZE_PATTERNS: Array<[RegExp, string]> = [
  [/\bon\w+\s*=/i, 'Inline event handler (use data-action)'],
  [/javascript\s*:/i, 'javascript: URI'],
  [/\beval\s*\(/, 'eval() call'],
  [/\bnew\s+Function\s*\(/, 'new Function() call'],
  [/\bdocument\.cookie\b/, 'document.cookie access'],
  [/\b(localStorage|sessionStorage)\b/, 'Browser storage access'],
  [/\b(fetch|XMLHttpRequest)\s*\(/, 'Direct network access'],
  [/new\s+WebSocket\s*\(/, 'WebSocket constructor'],
  [/<\s*(script|iframe|object|embed)\b/i, 'Executable element in template'],
  [/\bsrcdoc\s*=/i, 'srcdoc attribute'],
  [/\bimport\s*\(/, 'Dynamic import()'],
  [/\brequire\s*\(/, 'Dynamic require()'],
];

function rejectKnownBadPatterns(code: string): void {
  for (const [pattern, reason] of SANITIZE_PATTERNS) {
    if (pattern.test(code)) {
      throw new Error(`Component code rejected: ${reason}`);
    }
  }
}

function matrixActorInboxQueue(mount: string): string {
  const safeMount = mount.trim()
    .replace(/[^a-zA-Z0-9._-]/g, '_')
    .replace(/_+/g, '_')
    .replace(/^_+|_+$/g, '');
  return `matrix-actor-${safeMount || 'root'}`;
}

// ═══════════════════════════════════════════════════════════════════════════
// MatrixActor
// ═══════════════════════════════════════════════════════════════════════════

export abstract class MatrixActor {
  /**
   * Index signature for dynamic handler method access.
   * Enables type-safe lookup of onFoo handlers by name.
   */
  [key: string]: unknown;

  /**
   * Static metadata declaring what DOMAIN commands this actor accepts.
   * System commands ($join, $leave, introspect, $ping) are auto-subscribed.
   * Override in subclass.
   */
  static accepts: Record<string, unknown> = {};

  /**
   * Static metadata declaring what events this actor emits.
   * Override in subclass.
   */
  static emits: Record<string, unknown> = {};

  /**
   * Declarative subscription to events from other actors.
   *
   * The framework auto-subscribes and dispatches to `on{OpName}` handlers.
   * Keys are semantic topics (mount/$events), values have optional filter and description.
   *
   * ```ts
   * static subscribes = {
   *   'system.harness.loop/$events': {
   *     filter: ['harness.iterationComplete'],
   *     description: 'Live updates from the optimization loop',
   *   },
   * };
   *
   * onHarnessIterationComplete(payload) { ... }
   * ```
   */
  static subscribes: Record<string, { filter?: string[]; description?: string }> = {};

  /**
   * Human-readable self-description surfaced through introspection.
   */
  static description?: string;

  /**
   * Declarative sibling tracks used by shell-level CQRS view wiring.
   */
  static tracks?: string[];

  /**
   * Unified contract declaration (COM-inspired).
   * Groups ops by interface (dot-prefix), with payload/response/error schemas.
   * When set, $introspect returns the contract. Replaces scattered accepts/emits.
   * Optional — actors without contract still work via static accepts.
   *
   * See: ARCHITECTURE/ARCHITECTURE-SERVICE-CONTRACTS.md
   */
  static contract?: import('../engine/core/ServiceContract').ActorContract;

  /**
   * Interactive mode declaration for the Universal CLI.
   * When a user runs `matrix <address>` without an explicit op,
   * the CLI checks this field to determine behavior:
   *
   *   - mode: 'repl' — enter a readline → eval → print loop
   *   - mode: 'stream' — subscribe to actor's $events and print
   *   - mode: null — no interactive mode, fall back to $introspect
   *
   * See: ARCHITECTURE/system/ARCHITECTURE-UNIVERSAL-CLI.md §3
   */
  static interactive?: {
    mode: 'repl' | 'stream' | null;
    prompt?: string;
    sessionOp?: string;
    sessionIdField?: string;
    codeField?: string;
    topic?: string;
  } | null;

  /**
   * Theme tokens this actor declares support for.
   * Each key is a token name (without --mx- prefix), value is metadata.
   * Used by the $tokens handler and surfaced in $introspect / oracle snapshots.
   * Subclass declarations merge with (and override) parent declarations.
   */
  static tokens: Record<string, { default: string; description: string }> = {};

  /**
   * Output schemas per op. Describes what each handler returns.
   * Surfaced via $introspect { depth: 'full' } alongside acceptsSchema.
   */
  static outputSchemas?: Record<string, Record<string, unknown>>;

  /**
   * Error specifications per op. Documents known error conditions.
   * Each entry is an array of { code, description } objects.
   */
  static errorSpecs?: Record<string, Array<{ code: string; description: string }>>;

  /**
   * Optional self-documenting examples surfaced via $introspect { depth: 'full' }.
   * Per-op examples with input/output pairs for AI agent discovery.
   */
  static examples?: Record<string, Array<{ input: Record<string, unknown>; output?: Record<string, unknown> }>>;

  /**
   * Optional intent filters for capability-based resolution.
   */
  static intentFilters?: unknown[];

  /**
   * Optional high-level kind marker surfaced via $introspect.
   * Used by generic tooling to identify specialized actor patterns.
   */
  static kind?: string;

  /**
   * Higher-level skill bundles that compose multiple accepts into meaningful actions.
   * Skills are the primary discovery surface for AI agents (R18).
   * Each key is a skill name, value describes what it does and which ops it composes.
   *
   * See: ARCHITECTURE-SKILLS.md §2
   */
  static skills: Record<string, {
    description: string;
    ops?: string[];
    examples?: Array<{ input: Record<string, unknown>; expected?: string }>;
  }> = {};

  /**
   * Optional state schema for named type matching.
   */
  static stateSchema?: Record<string, unknown>;

  /**
   * Persistence scope for this actor's state.
   * Determines the KV key prefix used by NatsStateTap and the cascade
   * resolution order used by BlackboardActor on read.
   *
   * Scopes (most specific → least specific):
   *   session   — per-session (default). Survives F5 reload.
   *   user      — per-user. Survives new sessions. Use for preferences.
   *   page      — per-page/tab. Scoped to a single browser page.
   *   workspace — per-workspace. Shared across workspace members.
   *   org       — per-organization. Company defaults.
   *   root      — per-root. Root-wide shared state.
   *   global    — cross-root. Federation-wide.
   */
  static stateScope: 'session' | 'user' | 'page' | 'workspace' | 'org' | 'root' | 'realm' | 'global' = 'session';

  /**
   * Whether this actor's state should be persisted to KV.
   * Set to false for ephemeral actors (loading spinners, animations).
   */
  static persistState: boolean = true;

  // ═══════════════════════════════════════════════════════════════════════
  // Cognitive fields (Phase 1: Actor Agency)
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Actor's declared purpose — short text describing what this actor does and why.
   * Used by OraclePromptBuilder in system prompts. Surfaced via $introspect.
   * Default: undefined (passive actor with no declared purpose).
   */
  static purpose?: string;

  /**
   * Custom system prompt fragment injected when this actor runs an oracle loop.
   * Appended after the standard system prompt, before the component tree.
   * Default: undefined (no custom system prompt).
   */
  static systemPrompt?: string;

  /**
   * Memory scope: how far back and broadly this actor can query its own records.
   * 'session' = current session only, 'actor' = all sessions for this mount,
   * 'subtree' = this mount + children.
   * Default: undefined (no memory queries).
   */
  static memoryScope?: 'session' | 'actor' | 'subtree';

  /**
   * Operational regime controlling how strictly this actor follows its purpose.
   * Cascades downward — children inherit parent's regime if more restrictive (C10).
   * 'explore' = least restrictive, 'focused' = balanced, 'strict' = most restrictive.
   */
  static regime: 'explore' | 'focused' | 'strict' = 'explore';

  /**
   * Whether this actor can be prompted (runs an oracle loop).
   * When false (default), zero cognitive overhead — no oracle loop, no trigger
   * subscriptions, no memory queries, no plan tracking, no budget tracking (C8).
   */
  static promptable: boolean = false;

  /**
   * Declarative event-to-prompt bindings (Phase 2: Event-Driven Triggers).
   * Each entry specifies an event pattern, a prompt to execute, debounce, and budget.
   *
   * Event patterns:
   *   'timer:30s'              — SchedulerActor fires every 30s
   *   'child.error'            — Any child emits 'error'
   *   'child.{id}.error'       — Specific child emits 'error'
   *   'child.*'                — Any child event
   *   '$stateChanged'          — Own state changed via setState()
   *   'fact.updated:{domain}'  — BlackboardActor fact changed
   *
   * Default: undefined (no triggers, zero overhead).
   */
  static promptTriggers?: Array<{
    event: string;
    prompt: string;
    condition?: string;    // JS predicate on payload: 'payload.value > 40'
    debounceMs?: number;   // Min interval between fires (default: 5000)
    maxPerHour?: number;   // Per-trigger budget cap (default: 60)
  }>;

  /**
   * Configurable child event whitelist. Events not in this list are
   * filtered out from child $events subscriptions.
   * Default: ['ready', 'disposed', 'error']
   */
  static childEventWhitelist?: string[];

  /**
   * How this actor continues across context deaths.
   * 'stateless' = no recovery, 'session' = within session, 'persistent' = survives restart.
   */
  static continuationType: 'stateless' | 'session' | 'persistent' = 'stateless';

  /**
   * Plan persistence scope.
   * 'session' = ephemeral (default), 'actor' = survives daemon restart.
   */
  static planScope: 'session' | 'actor' = 'session';

  // ═══════════════════════════════════════════════════════════════════════
  // Core fields (from MatrixComponent)
  // ═══════════════════════════════════════════════════════════════════════

  /** The injected context */
  protected _context?: IMatrixContext;

  /** Optional fact-plane emit capability token. */
  protected _factCapability: ICapabilityToken | null = null;

  /** Subscription cleanup bag */
  protected _subs = new SubscriptionBag();

  /** Phase 2: Prompt trigger subscriptions (lazy — only allocated when promptTriggers declared) */
  private _triggerSubs?: Map<string, {
    event: string;
    unsub: () => void;
    lastFiredAt: number;
    fireCount: number;
    hourBoundary: number;
    maxPerHour: number;
  }>;
  /** Phase 2: Debounce timers for prompt triggers */
  private _triggerTimers?: Map<string, ReturnType<typeof setTimeout>>;
  /** Runtime trigger definitions added via $triggers.add. */
  private _runtimePromptTriggers?: Map<string, NonNullable<typeof MatrixActor.promptTriggers>[0]>;

  /** Replica mounts for multi-bind state replication */
  protected _replicaMounts: string[] = [];

  /**
   * Runtime cognitive overrides (C21: actors evolve at runtime).
   * These overlay static class defaults. Set via $cognitive.set, persisted
   * to system.agents.memory for survival across restarts.
   */
  private _cognitiveOverrides: {
    purpose?: string;
    regime?: 'explore' | 'focused' | 'strict';
    promptable?: boolean;
    planScope?: 'session' | 'actor';
    memoryScope?: 'session' | 'actor' | 'subtree';
    systemPrompt?: string;
  } = {};

  /** Runtime skill overrides (added/removed via $skills.add/remove). */
  private _runtimeSkills: Record<string, { description: string; ops?: string[] }> = {};

  /**
   * Effective regime at runtime — may be promoted (more restrictive) by parent (C10).
   * Defaults to the static `regime` value. Children can be promoted but never demoted.
   */
  protected _effectiveRegime: 'explore' | 'focused' | 'strict' | undefined;

  /** Lamport logical clock for causal message ordering */
  protected _lamportClock = new LamportClock();

  /** Actor mailbox for exclusive message receiving */
  protected _mailbox?: IMailbox;

  /** Composed actor behavior (mailbox + dispatch) */
  protected _actor?: ActorBehavior;

  /** LISP dispatch strategy (if using LISP handlers) */
  private _lispStrategy?: LispDispatchStrategy;

  /** Shell metadata injected by MatrixActorHtmlElement for introspect */
  _shellInfo?: {
    shellTag: string;
    moduleUrl?: string;
    description?: string;
  };

  /** Shell element reference — set by MatrixActorHtmlElement during _receiveContext.
   *  Used by $source-code to return the shell's source (LLM-written code) instead of
   *  the inner actor's source (minified DynamicActor). */
  _shellElement?: unknown;

  // ═══════════════════════════════════════════════════════════
  // Membrane (CESK dispatch interception — Ring 0)
  // ═══════════════════════════════════════════════════════════

  /** Membrane CESK stepper — runs Lisp programs per-message for security/tracing */
  protected _membrane: import('./membrane/MembraneStepper.js').MembraneStepper | null = null;

  /** Pre-compiled membrane programs inherited from ancestors — immutable by child.
   *  Array because ancestors' membranes compose by sequential execution. */
  protected _inheritedMembranes: import('@open-matrix/omega-core').Expr[] = [];
  /** Source strings for inherited membranes (needed for propagation to children) */
  protected _inheritedMembraneSources: string[] = [];

  /** Pre-compiled local membrane program (set by actor) */
  protected _localMembrane: import('@open-matrix/omega-core').Expr | null = null;
  /** Source string for local membrane (needed for propagation to children) */
  protected _localMembraneSource: string | null = null;

  /** Whether the actor has been initialized */
  private _initialized = false;
  private _initializedAt = 0;

  /** Whether the actor has been bootstrapped */
  private _bootstrapped = false;

  /** Whether the actor has been disposed */
  private _disposed = false;
  /** True while the actor is running its own dispose cascade. */
  private _disposing = false;

  /** Callback for shell/adapter to receive state change notifications (FP-0.5). */
  _shellUpdateCallback: (() => void) | null = null;

  /** Reference to the shell element's ShadowRoot, so schema handlers can access DOM (e.g. iframes). */
  _shellRoot: ShadowRoot | null = null;

  /** Convenience getter for schema handler code: `this.shell.querySelector(...)` */
  get shell(): ShadowRoot | null { return this._shellRoot; }

  /** Promise that resolves when actor is ready */
  readonly ready: Promise<void>;
  private _resolveReady!: () => void;

  /** Actor name/id */
  private _name: string = '';

  // ═══════════════════════════════════════════════════════════════════════
  // Child management fields (LAZY — zero overhead if unused)
  // ═══════════════════════════════════════════════════════════════════════

  /** Child Manager — NOT allocated until first use */
  protected _children?: ChildManager;

  /** Supervisor behavior — NOT allocated until first use */
  protected _supervisor?: SupervisorBehavior;

  /** Security realm — NOT allocated until first use */
  protected _securityRealm?: SecurityRealm;

  /** Child event binding tracking */
  private _childBindings = new Map<string, {
    unsubs: Set<() => void>;
    bindings?: Record<string, string>;
  }>();

  /** Monitor references for tracked children */
  private _monitors = new Map<string, IMonitorRef>();

  /** Dynamic child metadata */
  private _dynamicMetadata = new Map<string, {
    id: string;
    isDynamic: boolean;
    shellTag?: string;
    codeHash?: string;
    createdAt?: number;
  }>();

  // ═══════════════════════════════════════════════════════════════════════
  // Observable state (NEW)
  // ═══════════════════════════════════════════════════════════════════════

  /** Observable state store */
  protected _state: Record<string, unknown> = {};

  /** Optional CQRS-style command history for inspector tooling. */
  protected _commandHistory: Array<{
    op: string;
    ts: number;
    from?: string;
  }> = [];

  /** Optional CQRS-style event history for inspector tooling. */
  protected _eventHistory: Array<{
    op: string;
    ts: number;
  }> = [];

  protected _historyLimit = 100;

  // ═══════════════════════════════════════════════════════════════════════
  // Constructor
  // ═══════════════════════════════════════════════════════════════════════

  constructor() {
    this.ready = new Promise<void>(resolve => {
      this._resolveReady = resolve;
    });
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Getters
  // ═══════════════════════════════════════════════════════════════════════

  get name(): string { return this._name; }
  get mount(): string { return this._context?.mount ?? ''; }
  get context(): IMatrixContext | undefined { return this._context; }
  get isInitialized(): boolean { return this._initialized; }
  get isReady(): boolean { return this._bootstrapped; }
  get isDisposed(): boolean { return this._disposed; }
  get mailbox(): IMailbox | undefined { return this._mailbox; }
  get lamportClock(): LamportClock { return this._lamportClock; }
  get replicaMounts(): readonly string[] { return this._replicaMounts; }
  get state(): Readonly<Record<string, unknown>> { return this._state; }

  /** Get the child manager (undefined if no children have been added) */
  protected get children(): ChildManager | undefined { return this._children; }

  // ═══════════════════════════════════════════════════════════════════════
  // Initialization
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Initialize the actor with a context.
   *
   * @param context - The matrix context
   * @param name - Optional actor name
   */
  async initialize(context: IMatrixContext, name?: string): Promise<void> {
    // Re-initialization (context change / remount)
    if (this._initialized) {
      if (this._context === context) return;
      this._unbindAll();
      this._context = context;

      this._subscribeToInbox();
      await this._flushTransportSubscriptions();
      await this._onContextChanged();
      return;
    }

    this._context = context;
    this._name = name ?? '';
    this._initialized = true;
    this._initializedAt = Date.now();

    // C10: Regime cascade — inherit parent's regime if more restrictive
    this._applyRegimeCascade();

    this._subscribeToInbox();
    await this._flushTransportSubscriptions();
    await this._bootstrap();
  }

  /** Regime ordering for cascade comparison (C10). */
  private static readonly _REGIME_ORDER: Record<string, number> = { explore: 0, focused: 1, strict: 2 };

  /**
   * Get the effective regime at runtime. May be promoted by parent cascade (C10).
   */
  get effectiveRegime(): 'explore' | 'focused' | 'strict' {
    return this._effectiveRegime ?? (this.constructor as typeof MatrixActor).regime;
  }

  /**
   * C10: Apply regime cascade from parent context.
   * Children inherit parent's regime if it's more restrictive.
   * The parent's regime is carried on the context via the 'parent-regime' service key.
   */
  private _applyRegimeCascade(): void {
    const ctor = this.constructor as typeof MatrixActor;
    const myRegime = this._cognitiveOverrides.regime ?? ctor.regime;
    const parentRegime = this._context?.getService<string>('parent-regime') ?? undefined;
    this._effectiveRegime = myRegime;
    if (parentRegime) {
      const order = MatrixActor._REGIME_ORDER;
      const parentLevel = order[parentRegime] ?? 0;
      const myLevel = order[myRegime] ?? 0;
      if (parentLevel > myLevel) {
        this._effectiveRegime = parentRegime as 'explore' | 'focused' | 'strict';
      }
    }
    // Register our effective regime for children to inherit
    if (this._context) {
      try {
        this._context.setService('parent-regime', this.effectiveRegime);
      } catch { /* context may not support setService */ }
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Lazy child management
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Lazy-initialize child management infrastructure.
   * Called on first addChild() or $join — zero overhead if never used.
   */
  protected _ensureChildManagement(): void {
    if (!this._children) {
      this._children = new ChildManager(this._context!);
      this._supervisor = new SupervisorBehavior(this._context!, this._children);
      // Use context's SecurityRealm if available (carries wired SpawnPolicy + PolicyEngine).
      // Fall back to default realm only if context has none.
      this._securityRealm = this._context?.securityRealm as SecurityRealm
        ?? new SecurityRealm(DEFAULT_SUPERVISION_POLICY, DEFAULT_SECURITY_POLICY);
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // System command handlers (auto-subscribed, NOT in static accepts)
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Handle $join handshake from child shells.
   * Lazy-initializes child management on first join.
   */
  on$join(payload: { id: string; mount: string; componentClass?: string; props?: unknown }): void {
    this._ensureChildManagement();
    if (!this._context) return;

    const { id, mount, componentClass } = payload;
    const wireInfo = this._context.transport.getWireTopicInfo?.(this.mount);
    const supervisorWireTopic = wireInfo?.wireTopic ?? this.mount;
    const transport = this._context.transport;

    // Register externally-created child shells so introspection and CLI discovery can walk the page tree.
    if (!this._children!.hasChild(id)) {
      this._children!.registerChild(id, mount, 'default', componentClass || 'unknown', 'append');
      this._supervisor?.registerChild(id, mount);

      const childInfo = {
        unsubs: new Set<() => void>(),
      };
      this._childBindings.set(id, childInfo);
      this._subscribeToChildEventsFromMeta(id, mount, 'default');
      this._monitorChild({ id, mount, unsubs: childInfo.unsubs });
    }

    const ackPayload = {
      status: 'ok',
      supervisor: this.mount,
      supervisorWireTopic,
      root: wireInfo?.root ?? wireInfo?.realm,
      realm: wireInfo?.root ?? wireInfo?.realm, // backward compat
      sessionId: wireInfo?.sessionId,
    };

    // NEW: MxEnvelope to child's $inbox
    const ackEnvelope: MxEnvelope = {
      op: '$join_ack',
      payload: ackPayload,
      from: this.mount,
    };
    transport.publish(TopicRouter.inbox(mount), ackEnvelope);

    // Propagate membrane to joining child (Phase 3 zone inheritance)
    this._propagateMembraneToChild(mount);
  }
  /**
   * Handle $leave when a child disconnects voluntarily.
   */
  on$leave(_payload: { id: string; mount: string }): void {
    // Child left voluntarily — no action needed
  }

  /**
   * Handle $ping liveness check.
   */
  on$ping(_payload?: unknown): { status: string; mount: string } {
    return { status: 'ok', mount: this.mount };
  }

  /**
   * Handle $status substrate self-report.
   */
  on$status(): ActorStatusSnapshotV1 {
    const now = Date.now();
    const runtimeMetadata = this._context?.getService<IComponentRuntimeMetadata>(COMPONENT_RUNTIME_METADATA);
    const lastCommand = this._commandHistory[this._commandHistory.length - 1];
    return {
      mount: this.mount,
      kind: this._statusKind(runtimeMetadata),
      uptime: this._initializedAt > 0 ? Math.max(0, now - this._initializedAt) : 0,
      lastMessageAt: lastCommand?.ts ?? null,
      runtimeId: this._statusRuntimeId(),
      packageId: runtimeMetadata?.packageName ?? null,
    };
  }

  /**
   * Handle $health substrate self-report.
   *
   * Actors override this for dependency-specific checks. The base actor is
   * healthy when it is initialized and not disposed.
   */
  on$health(): HealthSnapshotV1 {
    return {
      status: this._initialized && !this._disposed ? 'ok' : 'stopped',
      updatedAt: Date.now(),
    };
  }

  private _statusKind(runtimeMetadata?: IComponentRuntimeMetadata): string {
    const ctor = this.constructor as typeof MatrixActor;
    return ctor.kind ?? runtimeMetadata?.componentType ?? 'actor';
  }

  private _statusRuntimeId(): string | null {
    const runtimeId = this._readRuntimeIdService('runtimeId')
      ?? this._readRuntimeIdService('runtime-id');
    if (runtimeId) {
      return runtimeId;
    }
    const match = /^system\.runtimes\.([^.]+)(?:\.|$)/.exec(this.mount);
    return match?.[1] ?? null;
  }

  private _readRuntimeIdService(key: string): string | null {
    const value = this._context?.getService<unknown>(key);
    return typeof value === 'string' && value.trim() ? value.trim() : null;
  }

  // on$preferences and on$config — see G11 implementations below (lines ~595+)

  /**
   * Handle $join_ack from parent (when this actor is a child).
   * Stub in Phase 20a-1 — full implementation in MatrixActorHtmlElement.
   */
  on$joinAck(_payload: unknown): void {
    // Will be used by shells sending $join and waiting for ack
  }

  /**
   * Handle $membrane-inherit from parent — sets the inherited membrane.
   * This is called by the parent after child creation to propagate zone rules.
   * The inherited membrane is immutable by the child (no public setter).
   *
   * AUTH: Only accepts from the actor's parent mount (derived from own mount path).
   */
  'on$membraneInherit'(payload: { sources: string[] }, meta?: { from?: string }): void {
    if (!payload?.sources?.length) return;
    // Auth: only parent can set inherited membrane.
    // Require explicit sender identity — empty/undefined sender is rejected.
    const sender = meta?.from ?? '';
    if (!sender) return; // no sender identity → reject
    const parentMount = this._deriveParentMount();
    if (parentMount && sender !== parentMount) return; // not the parent → reject
    if (!this._membrane) {
      this._membrane = MembraneStepper.create();
    }
    this._inheritedMembraneSources = payload.sources;
    this._inheritedMembranes = payload.sources.map(s => this._membrane!.compile(s));
  }

  /**
   * Handle $membrane — introspect/modify membrane programs (inherited + local).
   * Used by CLI: `matrix membrane <mount>`
   *
   * AUTH: Only self, parent, or system actors can set/clear. Anyone can read (get).
   */
  'on$membrane'(_payload?: { action?: 'get' | 'set-local' | 'clear-local'; source?: string }, meta?: { from?: string }): unknown {
    const action = _payload?.action ?? 'get';
    if (action === 'set-local' || action === 'clear-local') {
      const sender = meta?.from ?? '';
      const isSelf = sender === this.mount;
      const parentMount = this._deriveParentMount();
      const isParent = parentMount ? sender === parentMount : false;
      const isSystem = sender.startsWith('system.') || sender.startsWith('daemon.system');
      const isCli = sender === 'cli' || sender.startsWith('cli.') || sender.startsWith('daemon.cli');
      if (sender && !isSelf && !isParent && !isSystem && !isCli) {
        return { ok: false, error: 'UNAUTHORIZED: only self, parent, system, or CLI can modify membrane' };
      }
    }
    if (action === 'set-local' && _payload?.source) {
      this.setLocalMembrane(_payload.source);
      return { ok: true, action: 'set-local' };
    }
    if (action === 'clear-local') {
      this.clearLocalMembrane();
      return { ok: true, action: 'clear-local' };
    }
    // Default: return current membrane state (no auth required for read)
    return {
      ok: true,
      inherited: this._inheritedMembraneSources,
      local: this._localMembraneSource,
      hasMembraneEngine: this._membrane !== null,
    };
  }

  /**
   * Handle $reply for request/reply pattern.
   * Stub in Phase 20a-1 — full implementation in FP-20b.
   */
  on$reply(_payload: unknown): void {
    // Will be used for request/reply resolution
  }

  /**
   * Alias for protocol-style $introspect operation.
   * Supports payload shape: { depth: 'full' }.
   */
  on$introspect(payload?: { depth?: 'basic' | 'full' }): ReturnType<MatrixActor['onIntrospect']> {
    return this.onIntrospect(payload);
  }

  /**
   * $source-code — Return this actor's source code via constructor.toString().
   */
  on$sourceCode(): { source: string; mount: string; tag?: string } {
    // If we have a shell element, return its source (readable LLM-written code)
    // instead of the inner actor's source (minified DynamicActor class)
    const shell = this._shellElement as { constructor?: { toString(): string }; tagName?: string } | undefined;
    if (typeof shell?.constructor?.toString === 'function') {
      return {
        source: shell.constructor.toString(),
        tag: typeof shell.tagName === 'string' ? shell.tagName.toLowerCase() : undefined,
        mount: this._context?.mount ?? '',
      };
    }
    return {
      source: this.constructor.toString(),
      mount: this._context?.mount ?? '',
    };
  }

  /**
   * System-plane state snapshot for Actor Inspector.
   */
  on$getState(): Record<string, unknown> {
    return {
      state: this._toSerializable(this._state),
    };
  }

  /**
   * System-plane history snapshot for CQRS log viewers.
   */
  on$getHistory(): {
    commands: Array<{ op: string; ts: number; from?: string }>;
    events: Array<{ op: string; ts: number }>;
  } {
    return {
      commands: [...this._commandHistory],
      events: [...this._eventHistory],
    };
  }

  /**
   * System-plane handler for activity frames pushed via `activityTo`.
   * Default is no-op. Browser elements and subclasses override for UI feedback.
   */
  // IMPORTANT: toHandlerCase('$activity') → '$activity' (lowercase 'a' after $).
  // The dispatch table looks up on$activity, NOT on$Activity.
  // MatrixActorHtmlElement uses explicit frameworkOps map so casing doesn't matter there.
  on$activity(_payload: unknown): void {
    // No-op — override in subclass or MatrixActorHtmlElement
  }

  /**
   * G11: Query effective preferences for this actor's mount.
   * Delegates to system.preferences service via RequestReply.
   */
  async on$preferences(payload?: { key?: string }): Promise<Record<string, unknown>> {
    const endpoint = this.context?.getService<string>('preferences-endpoint');
    if (!endpoint) return { ok: false, error: 'no preferences service' };
    try {
      return await RequestReply.execute(this.context!, endpoint, 'preferences.getEffective', {
        mount: this._mount, key: payload?.key,
      }, { timeoutMs: 3000 }) as Record<string, unknown>;
    } catch (err) {
      return { ok: false, error: err instanceof Error ? err.message : String(err) };
    }
  }

  /**
   * G11: Query effective config for this actor's mount.
   * Delegates to system.config service via RequestReply.
   */
  async on$config(payload?: { key?: string }): Promise<Record<string, unknown>> {
    const endpoint = this.context?.getService<string>('config-endpoint');
    if (!endpoint) return { ok: false, error: 'no config service' };
    try {
      return await RequestReply.execute(this.context!, endpoint, 'config.getEffective', {
        mount: this._mount, key: payload?.key,
      }, { timeoutMs: 3000 }) as Record<string, unknown>;
    } catch (err) {
      return { ok: false, error: err instanceof Error ? err.message : String(err) };
    }
  }

  /**
   * Factory: create a child actor from a code string.
   *
   * Headless (daemon) path: evals the code into a class, uses addChild()
   * to create, initialize, and register the child in ChildManager.
   *
   * MatrixActorHtmlElement overrides this with DOM concerns (custom element
   * registration, shadow DOM insertion, versioned tag names).
   */
  async onFactoryCreateFromCode(
    payload: { name: string; code: string }
  ): Promise<Record<string, unknown>> {
    const { name, code } = payload;
    if (!name || !code) {
      return { ok: false, error: 'factory.create-from-code requires name and code' };
    }
    if (!this._context) {
      return { ok: false, error: 'no context — actor not initialized' };
    }

    // Security gate: check if dynamic compilation is allowed in this policy
    if (!this._context.runtime.isDynamicCompilationAllowed?.()) {
      return { ok: false, error: 'Dynamic component compilation is DISABLED in this security policy.' };
    }

    const childId = `component-${name}`;

    rejectKnownBadPatterns(code);

    // Eval the code string into a class extending MatrixActor
    let ActorClass: any;
    try {
      // eslint-disable-next-line no-new-func
      ActorClass = new Function('MatrixActor', `return (${code})`)(
        this.constructor // pass actual class so code can extend it
      );
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      return {
        ok: false,
        error: {
          code: 'FACTORY_ACTOR_EVAL_FAILED',
          message: `Failed to evaluate actor code for "${name}".`,
          stage: 'eval',
          retryable: false,
          target: this._mount,
          op: 'factory.create-from-code',
          name,
          cause: {
            code: err instanceof Error && err.name ? err.name : 'EVAL_ERROR',
            message,
          },
        },
      };
    }

    // Auto-derive static accepts from onXxx methods if not set
    if (!ActorClass.accepts || Object.keys(ActorClass.accepts).length === 0) {
      const derived: Record<string, Record<string, never>> = {};
      for (const key of Object.getOwnPropertyNames(ActorClass.prototype)) {
        if (key.startsWith('on') && key.length > 2 && typeof ActorClass.prototype[key] === 'function') {
          if (key === 'onMounted') continue;
          const op = key[2].toLowerCase() + key.slice(3);
          derived[op] = {};
        }
      }
      ActorClass.accepts = derived;
    }

    // Use addChild() — handles runtime creation, ChildManager, supervision
    let childMount = '';
    try {
      childMount = await this.addChild(childId, ActorClass);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      return {
        ok: false,
        error: {
          code: 'FACTORY_ACTOR_MOUNT_FAILED',
          message: `Actor "${name}" evaluated but failed to mount.`,
          stage: 'mount',
          retryable: false,
          target: this._mount,
          op: 'factory.create-from-code',
          name,
          cause: {
            code: err instanceof Error && err.name ? err.name : 'MOUNT_ERROR',
            message,
          },
        },
      };
    }

    return { ok: true, mount: childMount, name };
  }

  /**
   * factory.load — Load a saved component from ComponentStoreActor.
   *
   * Invokes component.get-source on system.component-store,
   * then delegates to factory.create-from-code.
   */
  async onFactoryLoad(
    payload: { name: string }
  ): Promise<Record<string, unknown>> {
    const { name } = payload ?? {};
    if (!name) {
      return { ok: false, error: 'factory.load requires name' };
    }
    if (!this._context) {
      return { ok: false, error: 'no context — actor not initialized' };
    }

    try {
      // Target the ComponentStoreActor through its configured service mount.
      const storeMount = this._context?.getService<string>('component-store-endpoint') ?? 'system.component-store';

      const result = await RequestReply.execute(
        this._context, storeMount, 'component.get-source', { name },
        { timeoutMs: 10_000 },
      ) as Record<string, unknown>;

      if (result.error) {
        return { ok: false, error: result.error };
      }

      const source = result.source as string;
      if (!source) {
        return { ok: false, error: `No source returned for component "${name}"` };
      }

      // Delegate to the universal pipeline
      return this.onFactoryCreateFromCode({ name, code: source });
    } catch (err) {
      return { ok: false, error: err instanceof Error ? err.message : String(err) };
    }
  }

  /**
   * component.save — Extract source from a running child and persist it.
   *
   * Finds the child by mount segment, invokes $source-code on it,
   * then sends component.persist to system.component-store.
   */
  async onComponentSave(
    payload: { mount: string; name?: string; surface?: 'dom' | 'headless'; description?: string }
  ): Promise<Record<string, unknown>> {
    const { mount } = payload ?? {};
    if (!mount) {
      return { ok: false, error: 'component.save requires mount' };
    }
    if (!this._context) {
      return { ok: false, error: 'no context — actor not initialized' };
    }

    try {
      // Get source code from the running component
      const sourceResult = await RequestReply.execute(
        this._context, mount, '$source-code', {},
        { timeoutMs: 5_000 },
      ) as Record<string, unknown>;

      const source = (sourceResult?.source as string) ?? (typeof sourceResult === 'string' ? sourceResult : null);
      if (!source) {
        return { ok: false, error: `No source returned from $source-code at ${mount}` };
      }

      // Introspect the live component to capture full metadata
      let accepts: Array<{ op: string; params?: Record<string, unknown> }> = [];
      let emits: string[] = [];
      let tracks: string[] = [];
      let liveDescription = '';
      try {
        const intro = await RequestReply.execute(
          this._context, mount, '$introspect', {},
          { timeoutMs: 3_000 },
        ) as Record<string, unknown>;
        const frameworkPrefixes = ['$', 'factory.', 'component.', 'snapshot.'];
        const rawAccepts = intro.accepts as string[] | undefined;
        if (Array.isArray(rawAccepts)) {
          accepts = rawAccepts
            .filter((op: string) => !frameworkPrefixes.some(p => op.startsWith(p)))
            .map((op: string) => ({ op }));
        }
        const rawEmits = intro.emits as string[] | Record<string, unknown> | undefined;
        if (Array.isArray(rawEmits)) emits = rawEmits;
        else if (rawEmits && typeof rawEmits === 'object') emits = Object.keys(rawEmits);
        const rawTracks = intro.tracks as string[] | undefined;
        if (Array.isArray(rawTracks)) tracks = rawTracks;
        liveDescription = (intro.description as string) ?? '';
      } catch { /* introspection failed — save without metadata */ }

      // Derive name from mount if not provided (strip "component-" prefix)
      const mountSegment = mount.split('.').pop() ?? mount;
      const componentName = payload.name ?? mountSegment.replace(/^component-/, '');
      const surface = payload.surface ?? 'headless';

      // Persist via ComponentStoreActor through its configured service mount.
      const storeMount = this._context?.getService<string>('component-store-endpoint') ?? 'system.component-store';

      const persistResult = await RequestReply.execute(
        this._context, storeMount, 'component.persist',
        {
          name: componentName,
          source,
          surface,
          accepts,
          emits,
          tracks,
          description: payload.description ?? liveDescription,
        },
        { timeoutMs: 10_000 },
      ) as Record<string, unknown>;

      return persistResult;
    } catch (err) {
      return { ok: false, error: err instanceof Error ? err.message : String(err) };
    }
  }

  /**
   * Framework op handler: $prompt (FP-0.4).
   *
   * Makes any MatrixActor promptable — forwards the prompt to the daemon.
   * MatrixActorHtmlElement overrides this with a richer implementation
   * (snapshot tree, session management, blocking mode).
   *
   * For dynamically created components this is the minimum needed to
   * receive and forward a prompt to the agent system.
   */
  /**
   * SEALED — subclasses MUST NOT override this method.
   *
   * $prompt is a framework extension method that EVERY actor gets. It builds
   * a context snapshot of THIS actor (accepts, emits, skills, state) and
   * forwards to system.agents for LLM processing. The agent sees this actor's
   * capabilities and can interact with it via tools.
   *
   * Domain-specific prompt handling belongs in named ops like 'session.start',
   * 'codex.prompt', etc. — NOT in on$prompt.
   *
   * If a subclass overrides on$prompt, this base implementation detects it
   * and runs the framework trampoline anyway, ignoring the override.
   */
  async on$prompt(payload: Record<string, unknown>): Promise<Record<string, unknown>> {
    const prompt = typeof payload?.prompt === 'string' ? payload.prompt : '';
    if (!prompt) {
      return { ok: false, code: 'MISSING_PROMPT', message: '$prompt requires a prompt string' };
    }
    if (!this._context) {
      return { ok: false, code: 'NO_CONTEXT', message: 'Component not initialized' };
    }

    const requestId = typeof payload.requestId === 'string'
      ? payload.requestId
      : `req-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;

    // Build minimal snapshot of this actor — OracleTreeNode shape
    const ctor = this.constructor as typeof MatrixActor;
    const acceptsRaw = ctor.accepts ?? {};
    const acceptsEntries = Object.entries(acceptsRaw).map(([op, paramDef]) => ({
      op,
      params: paramDef && typeof paramDef === 'object'
        ? Object.entries(paramDef as Record<string, string>).map(([k, v]) => [k, v] as [string, string])
        : [] as Array<[string, string]>,
    }));
    const snapshotTree = {
      mount: this._context.mount,
      tag: ctor.name,
      accepts: acceptsEntries,
      emits: Object.keys(ctor.emits ?? {}),
      skills: Object.keys(ctor.skills ?? {}),
      skillsSchema: ctor.skills ?? {},
      tokens: {} as Record<string, { default: string; description: string }>,
      cognitiveState: typeof (this as unknown as { getCognitiveState?: () => unknown }).getCognitiveState === 'function'
        ? (this as unknown as { getCognitiveState: () => unknown }).getCognitiveState()
        : {},
      children: [] as ICompositeIntrospectChild[],
    };

    // Resolve caller root from transport
    const transport = this._context.transport as unknown as { root?: string; realm?: string };
    const callerRoot = transport?.root ?? transport?.realm ?? 'unknown';
    // Discover LLM endpoint from context services (registered by daemon at startup).
    // Falls back to convention 'system.agents' if not registered.
    const llmTarget = this._context.getService<string>('llm-endpoint') ?? 'system.agents';

    // C9: Generate traceId for self-prompts so the full span chain is visible
    const traceId = typeof payload.traceId === 'string'
      ? payload.traceId
      : `trace-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;

    const promptPayload: Record<string, unknown> = {
      prompt,
      requestId,
      traceId,
      callerRoot,
      callerRealm: callerRoot, // backward compat
      callerMount: this._context.mount,
      snapshotTree,
      targetSnapshots: [snapshotTree],
      executionPolicy: payload.executionPolicy ?? {
        allowCommands: ['*'],
        denyCommands: [],
      },
    };

    // Forward activityTo so the agent can push $activity frames back to the caller.
    // Without this, non-blocking prompts from browser surfaces get no streaming response.
    if (Array.isArray(payload.activityTo) && payload.activityTo.length > 0) {
      promptPayload.activityTo = payload.activityTo;
    }

    // Forward sessionId so the agent can continue an existing session.
    // Without this, every prompt through the trampoline creates a new session.
    if (typeof payload.sessionId === 'string' && payload.sessionId) {
      promptPayload.sessionId = payload.sessionId;
    }

    // Forward principalId for session scoping.
    if (typeof payload.principalId === 'string' && payload.principalId) {
      promptPayload.principalId = payload.principalId;
    }

    // Blocking path (CLI callers): use RequestReply.execute() so the caller
    // gets the full agent result instead of just an ACK.
    if (payload.blocking) {
      promptPayload.blocking = true;
      try {
        const result = await RequestReply.execute(
          this._context, llmTarget, '$prompt', promptPayload,
          { timeoutMs: 300_000 },
        ) as Record<string, unknown>;
        return { ...result, requestId };
      } catch (err) {
        return {
          ok: false,
          code: 'BLOCKING_TIMEOUT',
          message: err instanceof Error ? err.message : String(err),
          requestId,
        };
      }
    }

    // Non-blocking path: fire-and-forget to daemon's inbox
    RequestReply.sendToInbox(this._context, llmTarget, '$prompt', promptPayload);
    return { ok: true, accepted: true, requestId };
  }

  /**
   * $skill — invoke a named skill on this actor.
   * Looks up the skill in static skills, builds a focused prompt from its
   * description, and routes through the standard $prompt pipeline.
   * Skills are focused prompt configurations — same oracle loop, scoped intent.
   */
  async on$skill(payload: Record<string, unknown>): Promise<Record<string, unknown>> {
    const skillName = typeof payload?.skill === 'string' ? payload.skill : '';
    if (!skillName) {
      return { ok: false, code: 'MISSING_SKILL', message: '$skill requires a skill name' };
    }

    const ctor = this.constructor as typeof MatrixActor;
    const skillDef = this._runtimeSkills[skillName] ?? ctor.skills?.[skillName];
    if (!skillDef) {
      const available = [...new Set([...Object.keys(ctor.skills ?? {}), ...Object.keys(this._runtimeSkills)])];
      return { ok: false, code: 'UNKNOWN_SKILL', message: `Unknown skill: ${skillName}`, available };
    }

    // Build focused prompt from skill definition + optional context
    const context = typeof payload.context === 'string' ? payload.context : '';
    const prompt = context
      ? `Execute skill "${skillName}": ${skillDef.description}\n\nContext: ${context}`
      : `Execute skill "${skillName}": ${skillDef.description}`;

    // Route through $prompt — same oracle loop, full trace chain (C9)
    return this.on$prompt({ ...payload, prompt, skillName });
  }

  // ── Cognitive Mutation Ops (C21: actors evolve at runtime) ──────────────

  /**
   * $cognitive.set — update cognitive fields at runtime.
   * Overlays static class defaults. Director uses this to edit actor config.
   * Persists to memory for survival across restarts (C21).
   */
  on$cognitiveSet(payload: Record<string, unknown>): Record<string, unknown> {
    if (typeof payload.purpose === 'string') this._cognitiveOverrides.purpose = payload.purpose;
    if (payload.regime === 'explore' || payload.regime === 'focused' || payload.regime === 'strict') {
      this._cognitiveOverrides.regime = payload.regime;
      this._applyRegimeCascade();
    }
    if (typeof payload.promptable === 'boolean') this._cognitiveOverrides.promptable = payload.promptable;
    if (payload.planScope === 'session' || payload.planScope === 'actor') this._cognitiveOverrides.planScope = payload.planScope;
    if (payload.memoryScope === 'session' || payload.memoryScope === 'actor' || payload.memoryScope === 'subtree') this._cognitiveOverrides.memoryScope = payload.memoryScope;
    if (typeof payload.systemPrompt === 'string') this._cognitiveOverrides.systemPrompt = payload.systemPrompt;

    // Persist to memory for C21 (survive restarts)
    if (this._context) {
      RequestReply.sendToInbox(this._context, 'system.agents.memory', 'memory.save', {
        mount: this._context.mount,
        content: `[cognitive.override] ${JSON.stringify(this._cognitiveOverrides)}`,
        tags: ['cognitive-override', 'runtime-config'],
      });
    }

    return { ok: true, overrides: { ...this._cognitiveOverrides } };
  }

  /** $cognitive.get — return effective cognitive state (runtime overrides + class defaults). */
  on$cognitiveGet(): Record<string, unknown> {
    const ctor = this.constructor as typeof MatrixActor;
    return {
      ok: true,
      purpose: this._cognitiveOverrides.purpose ?? ctor.purpose ?? '',
      systemPrompt: this._cognitiveOverrides.systemPrompt ? '(set)' : (ctor.systemPrompt ? '(set)' : ''),
      regime: this.effectiveRegime,
      promptable: this._cognitiveOverrides.promptable ?? ctor.promptable,
      memoryScope: this._cognitiveOverrides.memoryScope ?? ctor.memoryScope,
      planScope: this._cognitiveOverrides.planScope ?? ctor.planScope,
      continuationType: ctor.continuationType,
      skills: { ...ctor.skills, ...this._runtimeSkills },
      overrides: { ...this._cognitiveOverrides },
    };
  }

  /** $triggers.add — add a prompt trigger at runtime. */
  on$triggersAdd(payload: Record<string, unknown>): Record<string, unknown> {
    const event = typeof payload.event === 'string' ? payload.event : '';
    const prompt = typeof payload.prompt === 'string' ? payload.prompt : '';
    if (!event) return { ok: false, error: 'event required' };
    if (!prompt) return { ok: false, error: 'prompt required' };

    const trigger: NonNullable<typeof MatrixActor.promptTriggers>[number] = {
      event,
      prompt,
      debounceMs: typeof payload.debounceMs === 'number' ? payload.debounceMs : 5000,
      maxPerHour: typeof payload.maxPerHour === 'number' ? payload.maxPerHour : 60,
    };
    if (!this._runtimePromptTriggers) this._runtimePromptTriggers = new Map();
    this._runtimePromptTriggers.set(event, trigger);

    // Wire the trigger using existing infrastructure
    if (this._context) {
      if (!this._triggerSubs) { this._triggerSubs = new Map(); this._triggerTimers = new Map(); }
      // Delegate to existing wiring based on pattern
      const pattern = trigger.event;
      if (pattern.startsWith('timer:')) { this._wireTimerTrigger(trigger); }
      else if (pattern.startsWith('child.')) { this._wireChildEventTrigger(trigger); }
      else if (pattern === '$stateChanged') { this._wireSelfStateTrigger(trigger); }
      else if (pattern.startsWith('fact.updated:')) { this._wireFactTrigger(trigger); }
    }

    return { ok: true, event, wired: true };
  }

  /** $triggers.remove — remove a prompt trigger at runtime. */
  on$triggersRemove(payload: Record<string, unknown>): Record<string, unknown> {
    const event = typeof payload.event === 'string' ? payload.event : '';
    if (!event) return { ok: false, error: 'event required' };

    const sub = this._triggerSubs?.get(event);
    if (sub) {
      sub.unsub();
      this._triggerSubs!.delete(event);
    }
    const timer = this._triggerTimers?.get(event);
    if (timer) {
      clearTimeout(timer);
      this._triggerTimers!.delete(event);
    }
    this._runtimePromptTriggers?.delete(event);

    return { ok: true, event, removed: !!sub };
  }

  /** $triggers.list — list all active triggers with runtime state. */
  on$triggersList(): Record<string, unknown> {
    const triggers: Array<Record<string, unknown>> = [];
    if (this._triggerSubs) {
      for (const [event, sub] of this._triggerSubs) {
        triggers.push({
          event,
          fireCount: sub.fireCount,
          lastFiredAt: sub.lastFiredAt,
          maxPerHour: sub.maxPerHour,
          budgetRemaining: sub.maxPerHour - sub.fireCount,
        });
      }
    }
    return { ok: true, triggers, count: triggers.length };
  }

  /** $skills.add — register a skill at runtime. */
  on$skillsAdd(payload: Record<string, unknown>): Record<string, unknown> {
    const name = typeof payload.name === 'string' ? payload.name : '';
    const description = typeof payload.description === 'string' ? payload.description : '';
    if (!name) return { ok: false, error: 'name required' };
    if (!description) return { ok: false, error: 'description required' };

    const ops = Array.isArray(payload.ops) ? payload.ops.filter((o): o is string => typeof o === 'string') : undefined;
    this._runtimeSkills[name] = { description, ops };
    return { ok: true, name, registered: true };
  }

  /** $skills.remove — remove a runtime skill. */
  on$skillsRemove(payload: Record<string, unknown>): Record<string, unknown> {
    const name = typeof payload.name === 'string' ? payload.name : '';
    if (!name) return { ok: false, error: 'name required' };

    const existed = name in this._runtimeSkills;
    delete this._runtimeSkills[name];
    return { ok: true, name, removed: existed };
  }

  // ── Service Contract Resolution ──────────────────────────────────────────
  // See: ARCHITECTURE/ARCHITECTURE-SERVICE-CONTRACTS.md
  // resolve() walks the intent provider chain (context is provider #1).
  // remember() and recall() are convenience methods over the memory service.

  /**
   * Resolve a service by contract — walks intent provider chain.
   * Context cascade is provider #1 (fastest). Result is cached.
   */
  protected async resolveService(contract: import('../engine/core/ServiceContract').ServiceContract): Promise<import('../engine/core/ServiceContract').IActorService | undefined> {
    return this._context?.resolve?.(contract);
  }

  /**
   * Remember something — saves to the memory service via contract resolution.
   * The memory service is resolved from context (trickled down from root).
   * meta.from is set by the framework — scoping is automatic.
   */
  async remember(content: string, tags?: string[]): Promise<unknown> {
    const { IMemoryService } = await import('../engine/core/contracts/IMemoryService');
    const memory = await this.resolveService(IMemoryService);
    if (!memory) throw new Error('No memory service available in this context');
    return memory.invoke('memory.save', { content, tags });
  }

  /**
   * Recall memories by query — searches the memory service.
   */
  async recall(query: string, limit?: number): Promise<unknown> {
    const { IMemoryService } = await import('../engine/core/contracts/IMemoryService');
    const memory = await this.resolveService(IMemoryService);
    if (!memory) return [];
    return memory.invoke('memory.search', { query, limit });
  }

  /**
   * Log a structured message — delegates to logging service via contract.
   */
  async logMessage(level: string, message: string, data?: Record<string, unknown>): Promise<unknown> {
    const { ILoggingService } = await import('../engine/core/contracts/ILoggingService');
    const logging = await this.resolveService(ILoggingService);
    if (!logging) return;
    return logging.invoke('logging.query', { source: this._name, kind: [level], limit: 0 });
  }

  /**
   * Schedule a recurring or one-shot task — delegates to scheduler service.
   */
  async schedule(config: { target: string; op: string; intervalMs?: number; delayMs?: number; payload?: Record<string, unknown> }): Promise<unknown> {
    const { ISchedulerService } = await import('../engine/core/contracts/ISchedulerService');
    const scheduler = await this.resolveService(ISchedulerService);
    if (!scheduler) throw new Error('No scheduler service available');
    return scheduler.invoke('scheduler.create', config);
  }

  /**
   * Infer — single-turn LLM completion via inference service.
   */
  async infer(prompt: string, opts?: { model?: string; maxTokens?: number }): Promise<unknown> {
    const { IInferenceService } = await import('../engine/core/contracts/IInferenceService');
    const inference = await this.resolveService(IInferenceService);
    if (!inference) throw new Error('No inference service available');
    return inference.invoke('infer', { prompt, ...opts });
  }

  /**
   * Placeholder protocol hook for dynamic viewer/control creation.
   * Concrete runtimes may override this behavior.
   */
  on$createControl(payload?: unknown): {
    ok: false;
    op: '$createControl';
    message: string;
    requested?: unknown;
  } {
    return {
      ok: false,
      op: '$createControl',
      message: 'Dynamic control creation is not enabled for this actor.',
      requested: this._toSerializable(payload),
    };
  }

  /**
   * Introspect handler — returns actor capabilities.
   * Always available on every actor.
   */
  protected onIntrospect(payload?: { depth?: 'basic' | 'full' }): {
    mount: string;
    componentClass: string;
    accepts: string[];
    emits: string[];
    tracks: string[];
    acceptsSchema: Record<string, unknown>;
    emitsSchema: Record<string, unknown>;
    tokens: string[];
    tokensSchema?: Record<string, { default: string; description: string }>;
    children?: Array<ICompositeIntrospectChild>;
    shellTag?: string;
    moduleUrl?: string;
    description?: string;
    kind?: string;
    outputSchemas?: Record<string, Record<string, unknown>>;
    errorSpecs?: Record<string, Array<{ code: string; description: string }>>;
    examples?: Record<string, Array<{ input: Record<string, unknown>; output?: Record<string, unknown> }>>;
    testVectors?: unknown[];
    documentation?: string;
    intentFilters?: unknown[];
    skills?: string[];
    skillsSchema?: Record<string, unknown>;
    stateSchema?: Record<string, unknown>;
    stateScope?: string;
    persistState?: boolean;
    metadata?: IComponentRuntimeMetadata;
    // Cognitive fields (Phase 1: Actor Agency)
    purpose?: string;
    systemPrompt?: string;
    memoryScope?: string;
    regime?: string;
    promptable?: boolean;
    promptTriggers?: Array<{ event: string; prompt: string; debounceMs?: number }>;
    continuationType?: string;
    planScope?: string;
  } {
    const ctor = this.constructor as typeof MatrixActor;
    // Full-depth introspection payload marker:
    // { depth: 'full' }
    const includeFull = payload?.depth === 'full';
    // Framework ops available on all HTML elements (like $introspect)
    const frameworkOps = ['$style', '$tokens', '$prompt', 'snapshot.tree.oracle', 'factory.create-from-code', 'factory.load', 'component.save'];
    const frameworkSchema: Record<string, unknown> = {
      $style:  { description: 'Set CSS custom properties on this component for visual styling.', props: 'Record<string, string>' },
      $tokens: { description: 'Set design tokens (colors, spacing, typography) on this component.', tokens: 'Record<string, string>' },
      $prompt: { description: 'Send a natural language prompt to this actor. The framework builds a context snapshot and routes to the LLM agent.', prompt: 'string', sessionId: 'string?' },
      'snapshot.tree.oracle': { description: 'Returns the full cognitive state tree of this actor and all descendants — includes serialized domain state (message content, table data, component state) for each child. Use this to READ what an actor currently contains before taking action.' },
      'factory.create-from-code': { description: 'Create a new child component from inline code and mount it under this actor.', name: 'string', code: 'string' },
      'factory.load': { description: 'Load and mount a named component as a child of this actor.', name: 'string' },
      'component.save': { description: 'Save this component to the component store for reuse.', mount: 'string', name: 'string?', componentDescription: 'string?' },
    };

    const resolvedTokens = ctor.tokens ?? {};
    const desc = ctor.description ?? this._shellInfo?.description;
    const runtimeMetadata = this._context?.getService<IComponentRuntimeMetadata>(COMPONENT_RUNTIME_METADATA);
    const base: {
      mount: string;
      componentClass: string;
      description?: string;
      accepts: string[];
      emits: string[];
      tracks: string[];
      tokens: string[];
      skills: string[];
      stateScope: typeof ctor.stateScope;
      persistState: boolean;
      metadata?: IComponentRuntimeMetadata;
      purpose?: string;
      systemPrompt?: string;
      memoryScope?: typeof ctor.memoryScope;
      regime?: string;
      promptable?: boolean;
      promptTriggers?: typeof ctor.promptTriggers;
      triggerState?: Array<{ event: string; fireCount: number; lastFiredAt?: number; maxPerHour: number; budgetRemaining: number }>;
      continuationType: typeof ctor.continuationType;
      planScope: typeof ctor.planScope;
      acceptsSchema: Record<string, unknown>;
      emitsSchema: Record<string, unknown>;
      tokensSchema?: Record<string, { default: string; description: string }>;
      skillsSchema?: Record<string, unknown>;
      outputSchemas?: Record<string, Record<string, unknown>>;
      errorSpecs?: Record<string, Array<{ code: string; description: string }>>;
      examples?: Record<string, Array<{ input: Record<string, unknown>; output?: Record<string, unknown> }>>;
      testVectors?: unknown[];
      documentation?: string;
      intentFilters?: unknown[];
      stateSchema?: Record<string, unknown>;
      contract?: typeof ctor.contract;
      interactive?: typeof ctor.interactive;
    } = {
      mount: this._context?.mount ?? '',
      componentClass: ctor.name,
      ...(desc ? { description: desc } : {}),
      ...(ctor.kind ? { kind: ctor.kind } : {}),
      accepts: [...new Set([...Object.keys(ctor.accepts ?? {}), ...frameworkOps])],
      emits: Object.keys(ctor.emits ?? {}),
      tracks: Array.isArray(ctor.tracks) ? ctor.tracks : [],
      tokens: Object.keys(resolvedTokens),
      skills: [...new Set([...Object.keys(ctor.skills ?? {}), ...Object.keys(this._runtimeSkills)])],
      stateScope: ctor.stateScope,
      persistState: ctor.persistState,
      // Cognitive fields (Phase 1: Actor Agency) — runtime overrides overlay class defaults (C21)
      ...((this._cognitiveOverrides.purpose ?? ctor.purpose) ? { purpose: this._cognitiveOverrides.purpose ?? ctor.purpose } : {}),
      ...((this._cognitiveOverrides.systemPrompt || ctor.systemPrompt) ? { systemPrompt: '(set)' } : {}),
      ...((this._cognitiveOverrides.memoryScope ?? ctor.memoryScope) ? { memoryScope: this._cognitiveOverrides.memoryScope ?? ctor.memoryScope } : {}),
      regime: this.effectiveRegime,
      promptable: this._cognitiveOverrides.promptable ?? ctor.promptable,
      ...(ctor.promptTriggers ? { promptTriggers: ctor.promptTriggers } : {}),
      ...(this._triggerSubs?.size ? {
        triggerState: [...this._triggerSubs.entries()].map(([key, sub]) => ({
          event: key,
          fireCount: sub.fireCount,
          lastFiredAt: sub.lastFiredAt,
          maxPerHour: sub.maxPerHour,
          budgetRemaining: sub.maxPerHour - sub.fireCount,
        })),
      } : {}),
      continuationType: ctor.continuationType,
      planScope: ctor.planScope,
      acceptsSchema: { ...(ctor.accepts ?? {}), ...frameworkSchema },
      emitsSchema: ctor.emits ?? {},
      ...(runtimeMetadata ? { metadata: runtimeMetadata } : {}),
      ...(includeFull && {
        tokensSchema: resolvedTokens,
        skillsSchema: ctor.skills ?? {},
        outputSchemas: ctor.outputSchemas ?? {},
        errorSpecs: ctor.errorSpecs ?? {},
        examples: ctor.examples ?? {},
        testVectors: [],
        documentation: `${ctor.name} service documentation`,
        intentFilters: ctor.intentFilters ?? [],
        stateSchema: ctor.stateSchema ?? {},
      }),
    };

    // Include unified contract if actor declares one (COM-inspired)
    if (ctor.contract) {
      base.contract = ctor.contract;
    }

    // Include interactive mode declaration for Universal CLI
    if (ctor.interactive !== undefined) {
      base.interactive = ctor.interactive;
    }

    // Include shell info and children together (children should not be hidden by shell metadata)
    const withShell = this._shellInfo
      ? {
          ...base,
          shellTag: this._shellInfo.shellTag,
          moduleUrl: this._shellInfo.moduleUrl,
        }
      : base;

    // Include children if child management has been initialized
    if (this._children) {
      const children: Array<ICompositeIntrospectChild> = [];
      for (const child of this._children.getAllChildren()) {
        children.push({
          id: child.id,
          slot: child.slot,
          mount: child.mount,
          componentClass: child.componentClass || 'unknown',
          accepts: child.accepts ?? [],
          emits: child.emits ?? [],
          tracks: child.tracks ?? [],
          ...(child.metadata ? { metadata: child.metadata } : {}),
        });
      }
      return { ...withShell, children };
    }

    return withShell;
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Observable state (NEW)
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Update observable state and emit change events.
   * Emits $stateChanged with full state, and $propertyChanged per changed key.
   */
  setState(patch: Partial<Record<string, unknown>>): void {
    if (!this._context) {
      throw new Error('[MatrixActor] Cannot setState before initialization');
    }

    const oldState = { ...this._state };
    Object.assign(this._state, patch);

    const _sessionId = this._context?.getService<string>('session-id');
    const ctor = this.constructor as typeof MatrixActor;
    this.emit('$stateChanged', {
      state: { ...this._state },
      stateScope: ctor.stateScope,
      ...(_sessionId ? { sessionId: _sessionId } : {}),
    });

    for (const key of Object.keys(patch)) {
      if (oldState[key] !== this._state[key]) {
        this.emit('$propertyChanged', {
          propertyName: key,
          oldValue: oldState[key],
          newValue: this._state[key],
        });
      }
    }

    // Notify shell/adapter for reactive DOM updates (FP-0.5)
    if (this._shellUpdateCallback) this._shellUpdateCallback();
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Emit
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Emit an event from this actor.
   *
   * Publishes to:
   *   1. $events topic (MxEnvelope) — canonical event envelope
   *   2. Per-op emits topic (enriched payload)
   *   3. Replica mounts (both formats)
   */
  protected emit(event: string, payload?: unknown): void {
    if (!this._context) {
      throw new Error('[MatrixActor] Cannot emit before initialization');
    }
    if (this._disposed) {
      const runtimeLifecycle = this._context.runtime as { isRunning?: boolean; isShuttingDown?: boolean };
      if (this._disposing || runtimeLifecycle?.isShuttingDown === true || runtimeLifecycle?.isRunning === false) {
        return;
      }
      // eslint-disable-next-line no-console -- base actor lifecycle warning in kernel layer
      console.warn(`[MatrixActor] Cannot emit from disposed actor: ${this._name}`);
      return;
    }

    const lamport = this._lamportClock.send();
    const mount = this._context.mount;
    const transport = this._context.transport;

    // NEW: Publish MxEnvelope to $events
    const envelope: MxEnvelope = {
      op: event,
      payload: payload ?? {},
      lamport,
      from: mount,
    };
    try {
      const eventsTopic = TopicRouter.events(mount);
      transport.publish(eventsTopic, envelope);
      this._pushBoundedHistory(this._eventHistory, {
        op: event,
        ts: Date.now(),
      });

      // Replica mounts
      for (const replicaMount of this._replicaMounts) {
        const replicaEventsTopic = TopicRouter.events(replicaMount);
        transport.publish(replicaEventsTopic, envelope);
      }
    } catch (error) {
      if (isClosedTransportPublishError(error)) {
        return;
      }
      throw error;
    }

    // Notify shell/adapter — emit() often follows state mutations in timer callbacks
    // where the shell has no other way to know the DOM needs updating.
    if (this._shellUpdateCallback) this._shellUpdateCallback();
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Send
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Send a command to another actor by mount path (internal).
   *
   * Publishes MxEnvelope to target's $inbox.
   */
  protected sendTo(targetMount: string, command: string, payload?: unknown): void {
    if (!this._context) {
      throw new Error('[MatrixActor] Cannot send before initialization');
    }
    const transport = this._context.transport;
    const lamport = this._lamportClock.send();

    // NEW: MxEnvelope to $inbox
    const envelope: MxEnvelope = {
      op: command,
      payload: payload ?? {},
      lamport,
      from: this._context.mount,
    };
    const inboxTopic = TopicRouter.inbox(targetMount);
    transport.publish(inboxTopic, envelope);

  }

  /**
   * Send a command with three calling conventions:
   *
   * 1. Direct child: sendCommand('tree', 'ExpandDir', { path: '/src' })
   * 2. Relative path: sendCommand('./split.tree', 'ExpandDir', { path: '/src' })
   * 3. Tracked domain (2-arg): sendCommand('ExpandDir', { path: '/src' })
   *    — only supported in MatrixActorHtmlElement (view → tracked domain actor)
   *
   * Publishes MxEnvelope to target's $inbox.
   */
  sendCommand(toOrCmd: string, cmdOrData?: string | unknown, data?: unknown): void {
    if (!this._context) {
      throw new Error('[MatrixActor] Cannot sendCommand before initialization');
    }

    if (typeof cmdOrData === 'string') {
      // 3-arg form: (target, command, data)
      const target = toOrCmd;
      const cmd = cmdOrData;
      let targetMount: string;

      if (target.startsWith('./')) {
        // Relative path: resolve against own mount
        const relativePath = target.slice(2);
        targetMount = this._context.mount
          ? `${this._context.mount}.${relativePath}`
          : relativePath;
      } else {
        // Direct child ID: lookup in ChildManager
        const childMount = this.getChildMount(target);
        if (!childMount) {
          throw new Error(`[MatrixActor] Unknown child: ${target}`);
        }
        targetMount = childMount;
      }

      const transport = this._context.transport;
      const lamport = this._lamportClock.send();
      const payload = data ?? {};

      // NEW: MxEnvelope to $inbox
      const envelope: MxEnvelope = {
        op: cmd,
        payload,
        lamport,
        from: this._context.mount,
      };
      transport.publish(TopicRouter.inbox(targetMount), envelope);

    } else {
      // 2-arg form: (command, data) — sends to tracked domain (view pattern)
      // Only supported in MatrixActorHtmlElement which has tracks support
      throw new Error(
        '[MatrixActor] 2-arg sendCommand requires tracked domain (use MatrixActorHtmlElement with tracks)'
      );
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Cross-root invocation (NEW)
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Cross-root RPC. Delegates to FederationPeer via transport adapter.
   */
  async invoke(rootPath: string, op: string, data?: unknown): Promise<unknown> {
    if (!this._context) {
      throw new Error('[MatrixActor] Cannot invoke before initialization');
    }
    // invoke() is an optional extension on transport adapters (FederationPeer adds it)
    const transport = this._context.transport as unknown as Record<string, unknown>;
    if (typeof transport.invoke === 'function') {
      return (transport.invoke as Function)(rootPath, op, data);
    }
    throw new Error('[MatrixActor] Transport does not support invoke (no FederationPeer)');
  }

  /**
   * Publish a fact into the shared blackboard plane.
   *
   * Requires an emit-capable token in `_factCapability`.
   * Throws synchronously when capability is missing.
   */
  private _isFactDomainLiteralSegment(segment: string): boolean {
    return /^[A-Za-z0-9_-]+$/.test(segment);
  }

  private _isFactDomainVariableSegment(segment: string): boolean {
    return /^\{[A-Za-z0-9_-]+\}$/.test(segment);
  }

  private _splitFactDomainSegments(
    value: string,
    options?: { allowWildcardSegments?: boolean; allowVariableSegments?: boolean }
  ): string[] | null {
    const allowWildcardSegments = options?.allowWildcardSegments ?? false;
    const allowVariableSegments = options?.allowVariableSegments ?? false;
    const trimmed = value.trim();
    if (trimmed.length === 0) return null;
    const segments = trimmed.split('.');
    for (const segment of segments) {
      if (segment !== segment.trim()) return null;
      if (segment.length === 0) return null;
      if (segment === '*') {
        if (!allowWildcardSegments) return null;
        continue;
      }
      if (this._isFactDomainVariableSegment(segment)) {
        if (!allowVariableSegments) return null;
        continue;
      }
      if (!this._isFactDomainLiteralSegment(segment)) return null;
    }
    return segments;
  }

  private _matchesFactCapabilitySubject(subject: string, domain: string): boolean {
    const normalizedSubject = subject.trim();
    const normalizedDomain = domain.trim();

    if (normalizedSubject === '*') return true;
    if (normalizedSubject === normalizedDomain) return true;

    const subjectSegments = this._splitFactDomainSegments(normalizedSubject, {
      allowWildcardSegments: true,
      allowVariableSegments: true,
    });
    const domainSegments = this._splitFactDomainSegments(normalizedDomain);
    if (!subjectSegments || !domainSegments) return false;
    if (subjectSegments.length !== domainSegments.length) return false;

    for (let i = 0; i < subjectSegments.length; i += 1) {
      const subjectSegment = subjectSegments[i];
      if (subjectSegment === '*') continue;
      if (this._isFactDomainVariableSegment(subjectSegment)) continue;
      if (subjectSegment !== domainSegments[i]) return false;
    }

    return true;
  }

  private _canEmitFactDomain(domain: string): boolean {
    const capability = this._factCapability;
    if (!capability) return false;

    const rights = capability.rights;
    if (!Array.isArray(rights) || !rights.includes('emit')) return false;

    const normalizedDomain = domain.trim();
    if (!this._splitFactDomainSegments(normalizedDomain)) return false;

    // Honor capability revocation/expiry via SecurityRealm
    const securityRealm = this._context?.securityRealm;
    if (securityRealm && typeof securityRealm.isCapabilityValid === 'function' && !securityRealm.isCapabilityValid(capability)) {
      return false;
    }

    const rawSubject = capability.subject;
    if (typeof rawSubject !== 'string') return false;

    return this._matchesFactCapabilitySubject(rawSubject, normalizedDomain);
  }

  publishFact(domain: string, data: Record<string, unknown>): Promise<number> {
    const normalizedDomain = typeof domain === 'string' ? domain.trim() : '';
    if (!this._canEmitFactDomain(normalizedDomain)) {
      throw new Error(`[MatrixActor] No 'emit' capability for fact domain: ${normalizedDomain}`);
    }
    if (!this._context) {
      throw new Error('[MatrixActor] Cannot publishFact before initialization');
    }

    const publishFact = this._context.publishFact;
    if (typeof publishFact !== 'function') {
      throw new Error('[MatrixActor] Fact transport is unavailable in this context');
    }

    return Promise.resolve(publishFact.call(this._context, normalizedDomain, data, this.mount));
  }

  /**
   * Read the latest fact for a domain from the shared fact plane.
   */
  async readFact(domain: string): Promise<{ data: unknown; revision: number } | null> {
    if (!this._context) {
      throw new Error('[MatrixActor] Cannot readFact before initialization');
    }

    const readFact = this._context.readFact;
    if (typeof readFact !== 'function') {
      return null;
    }

    const result = await readFact.call(this._context, domain);
    return result ?? null;
  }

  /**
   * Subscribe to fact updates.
   *
   * Uses context-native subscription when available; otherwise falls back to
   * polling via readFact() to support transport layers without push hooks.
   */
  subscribeFact(domain: string, handler: (data: unknown) => void): () => void {
    if (!this._context) {
      throw new Error('[MatrixActor] Cannot subscribeFact before initialization');
    }

    const subscribeFact = this._context.subscribeFact;
    if (typeof subscribeFact === 'function') {
      const unsubscribe = subscribeFact.call(this._context, domain, handler);
      return typeof unsubscribe === 'function' ? unsubscribe : () => { /* no-op */ };
    }

    const readFact = this._context.readFact;
    if (typeof readFact !== 'function') {
      return () => { /* no-op */ };
    }

    let active = true;
    let lastRevision = -1;
    let inFlight = false;

    const poll = async (): Promise<void> => {
      if (!active || inFlight) return;
      inFlight = true;
      try {
        const fact = await readFact.call(this._context, domain);
        if (!active || !fact) return;
        if (fact.revision !== lastRevision) {
          lastRevision = fact.revision;
          handler(fact.data);
        }
      } catch {
        // Secondary observation path: swallow polling errors.
      } finally {
        inFlight = false;
      }
    };

    void poll();
    const intervalId = setInterval(() => { void poll(); }, 3000);

    if (
      typeof intervalId === 'object' &&
      intervalId !== null &&
      'unref' in intervalId &&
      typeof intervalId.unref === 'function'
    ) {
      intervalId.unref();
    }

    return () => {
      active = false;
      clearInterval(intervalId);
    };
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Child management (lazy-initialized)
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Add a child actor.
   * Lazy-initializes ChildManager, SupervisorBehavior, SecurityRealm on first call.
   *
   * @param id - Unique child identifier
   * @param ActorClass - Actor class to instantiate
   * @param options - Slot, position, props, bindings
   * @returns Mount path of the created child
   */
  async addChild<T extends MatrixActor>(
    id: string,
    ActorClass: new () => T,
    options: IAddChildOptions = {}
  ): Promise<string> {
    if (!this._context) {
      throw new Error('[MatrixActor] Cannot add child before initialization');
    }
    if (!this._context.runtime) {
      throw new Error('[MatrixActor] No runtime in context — cannot create children');
    }

    this._ensureChildManagement();

    const { slot = 'default', position = 'append', props = {}, shellTag, bindings } = options;

    // Check for duplicates
    if (this._children!.hasChild(id)) {
      // eslint-disable-next-line no-console -- duplicate child registration warning in base actor
      console.warn(`[MatrixActor] Child '${id}' already exists in ${this._name}`);
      return this._children!.getChildMount(id)!;
    }

    // Spawn policy check
    const spawnDecision = this._securityRealm!.evaluateSpawn({
      componentType: ActorClass.name,
      parentMount: this._context.mount,
      slot,
      props,
      requestor: {
        type: 'component',
        id: this._context.mount,
        trustLevel: 'user',
      },
    });

    if (!spawnDecision.allowed) {
      throw new Error(
        `[MatrixActor] Spawn denied for '${ActorClass.name}': ${spawnDecision.reason ?? 'policy violation'}`
      );
    }

    const finalProps = spawnDecision.modifiedProps ?? props;

    // Create via runtime (UI Control Inversion)
    const descriptor: IComponentDescriptor = {
      componentClass: ActorClass,
      props: finalProps,
    };
    const childMount = await this._context.runtime.createChild(this._context, id, descriptor);

    // Register in ChildManager
    this._children!.registerChild(
      id, childMount, slot, ActorClass.name, position, ActorClass, finalProps
    );

    // Register in SupervisorBehavior for restart tracking
    this._supervisor!.registerChild(id, childMount);

    // Set up child bindings
    const childInfo = {
      unsubs: new Set<() => void>(),
      bindings,
    };
    this._childBindings.set(id, childInfo);

    // Subscribe to child events
    this._subscribeToChildEvents(id, childMount, ActorClass, slot);

    // Monitor child for supervision
    this._monitorChild({ id, mount: childMount, unsubs: childInfo.unsubs });

    // Replica propagation
    if (this._replicaMounts.length > 0) {
      for (const parentReplicaMount of this._replicaMounts) {
        const childReplicaMount = `${parentReplicaMount}.${id}`;
        this._context.transport.publish(
          TopicRouter.inbox(childMount),
          { op: 'addReplicaBind', payload: { mount: childReplicaMount } } as MxEnvelope
        );
      }
    }

    // Propagate membrane to child (Phase 3 zone inheritance)
    this._propagateMembraneToChild(childMount);

    // Emit lifecycle event
    this.emit('lifecycle.childAdded', {
      id, slot, mount: childMount,
      componentClass: ActorClass.name,
      props, shellTag,
    });

    return childMount;
  }

  /**
   * Remove a child actor by id.
   */
  async removeChild(id: string): Promise<void> {
    if (!this._children) return;

    const mount = this._children.getChildMount(id);
    const slot = this._children.getChildSlot(id);

    if (!mount) {
      // eslint-disable-next-line no-console -- missing child disposal target should remain visible in kernel logs
      console.warn(`[MatrixActor] Child '${id}' not found in ${this._name}`);
      return;
    }

    // Unsubscribe from child events
    const bindings = this._childBindings.get(id);
    if (bindings) {
      for (const unsub of bindings.unsubs) {
        try { unsub(); } catch { /* ignore */ }
      }
      this._childBindings.delete(id);
    }

    // Dispose via runtime
    try {
      await this._context!.runtime.remove(mount);
    } catch (err) {
      // eslint-disable-next-line no-console -- child disposal failures need direct lifecycle diagnostics
      console.error(`[MatrixActor] Error disposing child '${id}':`, err);
    }

    // Cleanup registrations
    this._children.unregisterChild(id);
    this._dynamicMetadata.delete(id);
    this._monitors.delete(id);
    this._securityRealm?.resetChild(id);

    this.emit('lifecycle.childRemoved', {
      id,
      slot: slot ?? 'default',
      mount,
    });
  }

  /**
   * Get mount path of a child by id.
   */
  getChildMount(id: string): string | undefined {
    return this._children?.getChildMount(id);
  }

  /**
   * Get all child mount paths in a slot, in order.
   */
  getChildMountsInSlot(slot: string = 'default'): string[] {
    if (!this._children) return [];
    return this._children.getChildIds(slot)
      .map(id => this._children!.getChildMount(id))
      .filter((m): m is string => m !== undefined);
  }

  /**
   * Get child ids in order.
   */
  childIds(slot?: string): string[] {
    return this._children?.getChildIds(slot) ?? [];
  }

  /**
   * Alias for childIds().
   */
  getChildIds(slot?: string): string[] {
    return this.childIds(slot);
  }

  /**
   * Move a child to a new position within its slot.
   */
  async moveChild(id: string, position: Position): Promise<void> {
    if (!this._children?.hasChild(id)) {
      // eslint-disable-next-line no-console -- invalid child move requests should remain visible in kernel logs
      console.warn(`[MatrixActor] Child '${id}' not found in ${this._name}`);
      return;
    }
    this._children.moveChild(id, position);
  }

  /**
   * Send a command to a child by id (internal convenience).
   */
  protected sendToChild(childId: string, command: string, payload?: unknown): void {
    const mount = this.getChildMount(childId);
    if (!mount) {
      // eslint-disable-next-line no-console -- missing child send targets should remain visible in kernel logs
      console.warn(`[MatrixActor] Child '${childId}' not found for sendToChild`);
      return;
    }
    this.sendTo(mount, command, payload);
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Supervision
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Configure supervision policy.
   */
  setSupervisionPolicy(policy: ISupervisionPolicy): void {
    this._ensureChildManagement();
    this._securityRealm = new SecurityRealm(policy, DEFAULT_SECURITY_POLICY);
  }

  /**
   * Configure security policy.
   */
  setSecurityPolicy(policy: ISupervisorSecurityPolicy): void {
    this._ensureChildManagement();
    this._securityRealm = new SecurityRealm(DEFAULT_SUPERVISION_POLICY, policy);
  }

  /**
   * Configure both supervision and security policies.
   */
  setSecurityRealm(
    supervisionPolicy: ISupervisionPolicy,
    securityPolicy: ISupervisorSecurityPolicy
  ): void {
    this._ensureChildManagement();
    this._securityRealm = new SecurityRealm(supervisionPolicy, securityPolicy);
  }

  /**
   * Handle an exit signal from a child actor.
   * Applies supervision policy: restart, stop, escalate, or ignore.
   */
  async handleChildExit(signal: IExitSignal): Promise<void> {
    if (!this._children) return;

    const childInfo = this._children.getAllChildren().find(c => c.mount === signal.from);
    if (!childInfo) {
      // eslint-disable-next-line no-console -- supervision anomalies in base actor must remain visible
      console.warn(`[MatrixActor] Received exit signal from unknown child: ${signal.from}`);
      return;
    }

    const action = await this._securityRealm!.handleExitSignal(signal);

    switch (action) {
      case 'restart':
        await this._supervisor!.handleExit(signal);
        break;
      case 'stop':
        await this.removeChild(childInfo.id);
        break;
      case 'escalate':
        this.emit('supervision.error', {
          childId: childInfo.id,
          mount: signal.from,
          reason: signal.reason,
          error: signal.error,
        });
        break;
      case 'ignore':
      default:
        break;
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Deep template bindings (NEW)
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Wire bindings from the template to ANY descendant's events.
   * Used by the DSL compiler to connect template-level event bindings
   * to handler methods on the template author's actor.
   *
   * Unlike _subscribeToChildEvents() (direct children, handler cascade),
   * deep bindings use EXPLICIT handler mapping only — no cascade.
   */
  _wireDeepBindings(bindings: DeepBindingDescriptor[]): void {
    if (!this._context) return;

    // Group bindings by descendant mount for single $events subscription per descendant
    const bindingsByMount = new Map<string, Map<string, string>>();

    for (const binding of bindings) {
      const descendantMount = this._context.mount
        ? `${this._context.mount}.${binding.relativePath}`
        : binding.relativePath;

      let eventMap = bindingsByMount.get(descendantMount);
      if (!eventMap) {
        eventMap = new Map();
        bindingsByMount.set(descendantMount, eventMap);
      }
      eventMap.set(binding.event, binding.handler);
    }

    // Subscribe once per descendant to $events
    for (const [descendantMount, eventMap] of bindingsByMount) {
      const eventsTopic = TopicRouter.events(descendantMount);
      const off = this._context.transport.subscribe(eventsTopic, (payload, meta) => {
        const envelope = payload as MxEnvelope;
        if (!envelope || !envelope.op) return;

        const handlerName = eventMap.get(envelope.op);
        if (!handlerName) return;

        const handler = this[handlerName];
        if (typeof handler === 'function') {
          (handler as Function).call(this, envelope.payload, meta);
        }
      });
      this._subs.add(off);
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Dynamic children (opt-in domain commands)
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Protocol handler for addDynamicChild command.
   */
  async onAddDynamicChild(params: IDynamicComponentDefinition): Promise<{
    mount: string;
    adapterId: string;
    shellTag: string;
  }> {
    return this.addDynamicChild(params);
  }

  /**
   * Protocol handler for listDynamicChildren command.
   */
  onListDynamicChildren(): Array<{
    id: string;
    mount: string;
    shellTag?: string;
    codeHash?: string;
    createdAt?: number;
  }> {
    return this.listDynamicChildren();
  }

  /**
   * Protocol handler for removeDynamicChild command.
   */
  async onRemoveDynamicChild(params: { id: string }): Promise<{ success: boolean }> {
    const meta = this._dynamicMetadata.get(params.id);
    if (!meta) return { success: false };
    await this.removeChild(params.id);
    return { success: true };
  }

  /**
   * Add a dynamic child from code at runtime.
   */
  async addDynamicChild(params: IDynamicComponentDefinition): Promise<{
    mount: string;
    adapterId: string;
    shellTag: string;
  }> {
    if (!this._context) {
      throw new Error('[MatrixActor] Cannot add dynamic child before initialization');
    }

    const { id, code, template, styles, tag, shellTag } = params;
    const customTag = tag || shellTag;

    if (this._dynamicMetadata.has(id)) {
      // eslint-disable-next-line no-console -- duplicate dynamic child registration warning in kernel layer
      console.warn(`[MatrixActor] Dynamic child '${id}' already exists`);
      const existing = this._dynamicMetadata.get(id)!;
      const adapterId = existing.shellTag || `dynamic-${id}`;
      return {
        mount: this.getChildMount(id) ?? '',
        adapterId,
        shellTag: adapterId,
      };
    }

    try {
      const ComponentClass = this._compileDynamicComponent(code, template, styles);
      const codeHash = this._hashCode(code);
      const requestedId = customTag || `dynamic-${id}-${codeHash}`;

      const runtimeAdapterId = this._context.runtime.registerExternalAdapter?.(
        requestedId, ComponentClass, { template, styles, tag: requestedId }
      );
      const adapterId: string = runtimeAdapterId || requestedId;

      const mount = await this.addChild(id, ComponentClass, { shellTag: adapterId });

      this._dynamicMetadata.set(id, {
        id, isDynamic: true, shellTag: adapterId,
        codeHash, createdAt: Date.now(),
      });

      this.emit('lifecycle.dynamicChildCreated', {
        id, mount, shellTag: adapterId, codeHash,
        componentClass: ComponentClass.name,
      });

      return { mount, adapterId, shellTag: adapterId };
    } catch (err: unknown) {
      const errorMessage = err instanceof Error ? err.message : String(err);
      throw new Error(`Dynamic component creation failed: ${errorMessage}`);
    }
  }

  /**
   * List all dynamic children.
   */
  listDynamicChildren(): Array<{
    id: string;
    mount: string;
    shellTag?: string;
    codeHash?: string;
    createdAt?: number;
  }> {
    const result: Array<{
      id: string; mount: string;
      shellTag?: string; codeHash?: string; createdAt?: number;
    }> = [];

    for (const [id, meta] of this._dynamicMetadata) {
      const mount = this.getChildMount(id);
      if (mount) {
        result.push({
          id, mount,
          shellTag: meta.shellTag,
          codeHash: meta.codeHash,
          createdAt: meta.createdAt,
        });
      }
    }

    return result;
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Serialization
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Serialize the component tree to a descriptor.
   */
  toDescriptor(): IComponentTreeDescriptor {
    const slots: Record<string, IChildDescriptorEntry[]> = {};

    if (!this._children) return { slots };

    for (const slotName of this._children.slots) {
      const ids = this._children.getChildIds(slotName);
      slots[slotName] = ids.map(id => {
        const mount = this._children!.getChildMount(id);
        if (!mount) return null;
        const clsName = this._children!.getChildClass(id) ?? 'unknown';
        return { id, mount, componentClass: clsName } as IChildDescriptorEntry;
      }).filter((entry): entry is IChildDescriptorEntry => entry !== null);
    }

    return { slots };
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Actor mode (mailbox serialization)
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Enable actor mode — serialized message processing via mailbox.
   */
  protected enableActorMode(): void {
    if (!this._context) {
      throw new Error('[MatrixActor] Cannot enable actor mode before initialization');
    }
    if (this._actor) {
      throw new Error('[MatrixActor] Actor mode already enabled');
    }

    this._actor = new ActorBehavior(this._context);
    this._mailbox = this._actor.getMailbox();

    // LISP dispatch strategy
    if (this._context.lispEnv) {
      this._lispStrategy = new LispDispatchStrategy(
        this._context.mount, this._context.lispEnv, 10
      );
      this._actor.addDispatchStrategy(this._lispStrategy);
    }

    // Register TypeScript handlers
    const ctor = this.constructor as typeof MatrixActor;
    const allCommands = [...SYSTEM_COMMANDS, ...Object.keys(ctor.accepts ?? {})];

    for (const cmd of allCommands) {
      const handlerName = `on${toHandlerCase(cmd)}`;
      const handlerFn = this[handlerName];
      if (typeof handlerFn !== 'function') continue;

      const handler = handlerFn as ComponentHandler;
      this._actor.registerHandler(cmd, async (payload, meta) => {
        const componentMeta = {
          topic: meta.topic ?? TopicRouter.inbox(this._context!.mount),
          ts: meta.ts,
        };
        return handler.call(this, payload, componentMeta);
      });
    }

    this._actor.start();
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Replica bindings
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Add a replica binding for state replication.
   */
  async addReplicaBind(remoteMount: string): Promise<void> {
    if (!this._context) {
      throw new Error('[MatrixActor] Cannot add replica bind before initialization');
    }
    if (this._replicaMounts.includes(remoteMount) || remoteMount === this._context.mount) {
      return;
    }

    const ctor = this.constructor as typeof MatrixActor;
    const allCommands = [...SYSTEM_COMMANDS, ...Object.keys(ctor.accepts ?? {})];

    // Build dispatch table for replica handlers
    const replicaDispatch: Record<string, ComponentHandler> = {};
    for (const cmd of allCommands) {
      const handlerName = `on${toHandlerCase(cmd)}`;
      const handlerValue = this[handlerName];
      if (typeof handlerValue === 'function') {
        replicaDispatch[cmd] = handlerValue as ComponentHandler;
      }
    }

    // Single $inbox subscription for the replica mount
    const inboxTopic = TopicRouter.inbox(remoteMount);
    const off = this._context.transport.subscribe(inboxTopic, async (payload, meta) => {
      const envelope = payload as MxEnvelope;
      if (!envelope || !envelope.op) return;

      const handler = replicaDispatch[envelope.op];
      if (!handler) return;

      // Wrap replyTo/correlationId for request/reply support
      const hasReply = !!(envelope.replyTo && envelope.correlationId);
      const payloadForRR: unknown = hasReply
        ? { correlationId: envelope.correlationId, replyTo: envelope.replyTo, params: envelope.payload }
        : envelope.payload;

      try {
        await RequestReplyHelper.executeWithReplySafe(
          this._context!.transport, payloadForRR,
          async (params) => handler.call(this, params ?? envelope.payload, meta)
        );
      } catch (err) {
        // eslint-disable-next-line no-console -- replica handler failures need direct kernel diagnostics
        console.error(`[MatrixActor] Error in replica handler for '${envelope.op}':`, err);
      }
    });
    this._subs.add(off);

    this._replicaMounts.push(remoteMount);

    // Propagate to existing children
    if (this._children) {
      for (const child of this._children.getAllChildren()) {
        const childReplicaMount = `${remoteMount}.${child.id}`;
        this._context.transport.publish(
          TopicRouter.inbox(child.mount),
          { op: 'addReplicaBind', payload: { mount: childReplicaMount } } as MxEnvelope
        );
      }
    }
  }

  /**
   * Remove a replica binding.
   */
  removeReplicaBind(remoteMount: string): void {
    const idx = this._replicaMounts.indexOf(remoteMount);
    if (idx >= 0) {
      this._replicaMounts.splice(idx, 1);
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Lifecycle hooks
  // ═══════════════════════════════════════════════════════════════════════

  /** Override for initialization logic. */
  protected async onBootstrap(): Promise<void> {
    // Override in subclass
  }

  /** Override for cleanup logic. */
  protected onDispose(): void {
    // Override in subclass
  }

  /** Hook called when context changes (remount). */
  protected async _onContextChanged(): Promise<void> {
    await this._remountChildren();
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Dispose
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Dispose the actor and clean up all resources.
   */
  dispose(): void {
    if (this._disposed) return;

    // Emit lifecycle.removed before marking disposed (emit() guard checks _disposed)
    try {
      this.emit('lifecycle.removed', { name: this._name });
    } catch {
      // Transport may already be unavailable — best-effort
    }

    this._disposing = true;
    this._disposed = true;

    try {
      this.onDispose();
    } catch (err) {
      // eslint-disable-next-line no-console -- dispose hook failures need direct kernel diagnostics
      console.error(`[MatrixActor] Error in onDispose for ${this._name}:`, err);
    }

    // Dispose children
    if (this._children) {
      for (const child of this._children.getAllChildren()) {
        try { this.removeChild(child.id); } catch { /* ignore */ }
      }
    }

    // Stop actor behavior
    if (this._actor) {
      try { this._actor.stop(); } catch (err) {
        // eslint-disable-next-line no-console -- actor stop failures need direct kernel diagnostics
        console.error(`[MatrixActor] Error stopping actor for ${this._name}:`, err);
      }
    }

    // Dispose LISP strategy
    if (this._lispStrategy) {
      try { this._lispStrategy.dispose(); } catch (err) {
        // eslint-disable-next-line no-console -- lisp strategy disposal failures need direct kernel diagnostics
        console.error(`[MatrixActor] Error disposing LISP strategy for ${this._name}:`, err);
      }
    }

    // Phase 2: Clean up trigger subscriptions and debounce timers
    if (this._triggerSubs) {
      for (const [, sub] of this._triggerSubs) {
        try { sub.unsub(); } catch { /* ignore */ }
      }
      this._triggerSubs.clear();
    }
    if (this._triggerTimers) {
      for (const [, timer] of this._triggerTimers) {
        clearTimeout(timer);
      }
      this._triggerTimers.clear();
    }

    this._subs.dispose();
    this._disposing = false;
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Private: Bootstrap
  // ═══════════════════════════════════════════════════════════════════════

  private async _bootstrap(): Promise<void> {
    if (this._bootstrapped) return;
    this._bootstrapped = true;

    try {
      await this.onBootstrap();

      // Phase 2: Wire prompt triggers after bootstrap completes
      const ctor = this.constructor as typeof MatrixActor;
      if (ctor.promptable && ctor.promptTriggers?.length && this._context) {
        this._wirePromptTriggers(ctor.promptTriggers);
      }

      if (this._context) {
        this.emit('lifecycle.available', { name: this._name });
      }
    } finally {
      this._resolveReady();
    }
  }

  private async _flushTransportSubscriptions(): Promise<void> {
    await this._context?.transport.flush?.();
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Private: Subscription binding
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Subscribe to inbound messages via $inbox topic.
   *
   * Builds a dispatch table from static accepts + system commands.
   * Routes all inbound MxEnvelopes through a unified dispatch function.
   *
   * Single subscription: mount/$inbox — receives MxEnvelope format.
   */
  private _subscribeToInbox(): void {
    if (!this._context) return;

    const ctor = this.constructor as typeof MatrixActor;
    const mount = this._context.mount;
    const transport = this._context.transport;

    // Build accepted ops set: system + domain + framework
    const systemCommands: readonly string[] = SYSTEM_COMMANDS;
    const domainCommands = Object.keys(ctor.accepts ?? {});

    // Warn about accepts entries with no parameter schema (description-only).
    // Parameter schemas are required for agents to construct correct payloads.
    if (ctor.accepts) {
      for (const [op, def] of Object.entries(ctor.accepts)) {
        if (op.startsWith('$')) continue; // framework ops don't need user-facing schemas
        if (!def || typeof def !== 'object') continue;
        const keys = Object.keys(def as Record<string, unknown>);
        if (keys.length === 1 && keys[0] === 'description') {
          console.warn(
            `[MatrixActor] ${ctor.name}.accepts["${op}"] has no parameter schema — ` +
            `only description. Add parameter fields (e.g., { description: '...', myParam: 'string' }) ` +
            `so agents can construct correct payloads.`
          );
        }
      }
    }

    // Framework ops for standalone headless actors (daemon-side).
    // SKIP when inside a shell element — the shell's _wireShellAccepts handles these,
    // and dual-dispatch causes race conditions (both actor and shell fire for same op).
    const headlessFrameworkOps = this._shellInfo
      ? []  // Shell-managed actor — shell handles framework ops
      : ['factory.create-from-code', 'factory.load', 'component.save', '$prompt', '$source-code'];
    const allOps = new Set([...systemCommands, ...domainCommands, ...headlessFrameworkOps]);

    // Build dispatch table: op → handler
    const dispatchTable: Record<string, ComponentHandler> = {};
    const defaultHandlerValue = this['onMessage'];
    const defaultHandler = typeof defaultHandlerValue === 'function'
      ? defaultHandlerValue as ComponentHandler
      : undefined;

    // Sealed framework ops — ALWAYS use the base MatrixActor implementation,
    // even if a subclass illegally overrides them. $prompt is the framework
    // trampoline that builds context and routes to system.agents. Domain-specific
    // prompt handling belongs in named ops (session.start, codex.prompt, etc.).
    //
    // Exception: actors that declare `static isPromptRouter = true` are the agent
    // system itself (AgentsRoot, AgentActor) and legitimately handle $prompt.
    const isPromptRouter = (ctor as any).isPromptRouter === true;
    const sealedOps: Record<string, ComponentHandler | undefined> = isPromptRouter
      ? {}
      : { '$prompt': MatrixActor.prototype.on$prompt as unknown as ComponentHandler };

    for (const op of allOps) {
      if (sealedOps[op]) {
        const sealedHandler = sealedOps[op]!;
        // Check if subclass attempted override — warn loudly
        const handlerName = `on${toHandlerCase(op)}`;
        const subclassMethod = this[handlerName];
        const baseMethod = (MatrixActor.prototype as any)[handlerName];
        if (typeof subclassMethod === 'function' && subclassMethod !== baseMethod) {
          console.error(
            `[MatrixActor] SEALED: ${this.constructor.name} overrides on$prompt — ` +
            `this is IGNORED. Use a named domain op instead.`
          );
        }
        dispatchTable[op] = sealedHandler.bind(this);
      } else {
        const handlerName = `on${toHandlerCase(op)}`;
        const handlerValue = this[handlerName];
        if (typeof handlerValue === 'function') {
          dispatchTable[op] = handlerValue as ComponentHandler;
        } else if (defaultHandler) {
          dispatchTable[op] = defaultHandler;
        }
      }
    }

    // ── Observability: cache record store (undefined in browser — zero overhead) ──
    const _dispatchRecordStore = this._context?.getService<IRecordStore>(RECORD_STORE_KEY) ?? null;

    // ── Core dispatch function (shared by $inbox and direct dispatch paths) ──

    const dispatchEnvelope = async (
      envelope: MxEnvelope,
      meta: { topic: string; ts: number }
    ): Promise<void> => {
      // 1. Lamport clock update (from envelope field OR payload __lamportClock)
      if (typeof envelope.lamport === 'number') {
        this._lamportClock.receive(envelope.lamport);
      } else {
        this._receiveLamportClock(envelope.payload);
      }

      // 2. Guard: reject unknown ops
      if (!allOps.has(envelope.op)) return;

      this._pushBoundedHistory(this._commandHistory, {
        op: envelope.op,
        ts: meta.ts,
        from: envelope.from,
      });

      // ══════════ POLICY / MEMBRANE INSERTION POINT (ADR-025) ══════════
      // System commands exempt — prevents bricked actors when policy has errors
      const POLICY_EXEMPT_OPS = [
        '$membrane',
        '$membrane-inherit',
        '$ping',
        '$status',
        '$health',
        '$introspect',
        '$join',
        '$join_ack',
        '$leave',
      ];
      if (!POLICY_EXEMPT_OPS.includes(envelope.op)) {

        // Path 1: IPolicyEngine from SecurityRealm (ADR-025 provider pattern — preferred)
        const policyEngine = this._context?.securityRealm?.policyEngine;
        if (policyEngine) {
          const decision: IPolicyDecision = await Promise.race([
            Promise.resolve(policyEngine.evaluate({
              op: envelope.op,
              payload: envelope.payload,
              sender: envelope.from || '',
              meta: { topic: meta.topic, ts: meta.ts, correlationId: envelope.correlationId, replyTo: envelope.replyTo },
            })),
            new Promise<IPolicyDecision>((_, reject) =>
              setTimeout(() => reject(new Error('Policy engine timeout')), MatrixActor.MEMBRANE_TIMEOUT_MS)
            ),
          ]).catch((): IPolicyDecision => ({ action: 'allow' })); // fail-open

          if (decision.action === 'deny') {
            if (envelope.replyTo && envelope.correlationId) {
              RequestReplyHelper.sendErrorReply(
                transport,
                { correlationId: envelope.correlationId, replyTo: envelope.replyTo, params: null },
                new Error(decision.error || 'Denied by policy'),
              );
            }
            return; // Message blocked
          }
          if (decision.action === 'mutate') {
            envelope = { ...envelope, payload: decision.payload };
          }
        }

        // Path 2: Legacy membrane (per-actor Lisp — backward compat)
        if (this._membrane && (this._inheritedMembranes.length > 0 || this._localMembrane)) {
          const membraneResult = await this._runMembrane(envelope, meta);

          if (membraneResult.action === 'deny') {
            if (envelope.replyTo && envelope.correlationId) {
              RequestReplyHelper.sendErrorReply(
                transport,
                { correlationId: envelope.correlationId, replyTo: envelope.replyTo, params: null },
                new Error(membraneResult.error || 'Denied by membrane'),
              );
            }
            return; // Message blocked
          }
          if (membraneResult.action === 'mutate') {
            envelope = { ...envelope, payload: membraneResult.payload };
          }
        }
      }
      // ══════════ END POLICY / MEMBRANE ══════════

      // 2b. Observability: record dispatch (no-op when store is null, i.e. browser)
      if (_dispatchRecordStore) {
        try {
          _dispatchRecordStore.write({
            kind: 'dispatch',
            source: envelope.from ?? '',
            target: mount,
            op: envelope.op,
            phase: 'end',
            ts: meta.ts || Date.now(),
            correlationId: envelope.correlationId,
            traceId: envelope.traceId,
            spanId: envelope.spanId,
            parentSpanId: envelope.parentSpanId,
          });
        } catch (_) { /* never let instrumentation crash dispatch */ }
      }

      // 3. Build payload for RequestReplyHelper — if envelope carries
      //    replyTo/correlationId, wrap as IRequestReplyEnvelope so
      //    executeWithReply can detect and handle it.
      const hasReply = !!(envelope.replyTo && envelope.correlationId);
      const payloadForRR: unknown = hasReply
        ? { correlationId: envelope.correlationId, replyTo: envelope.replyTo, params: envelope.payload }
        : envelope.payload;

      // 4. Actor mode: queue to mailbox for serialized processing
      if (this._mailbox) {
        this._mailbox.send({
          command: envelope.op,
          payload: payloadForRR,
          replyTo: envelope.replyTo,
          meta: { ts: meta.ts, from: envelope.from ?? meta.topic, correlationId: envelope.correlationId },
        });
        return;
      }

      // 5. Direct mode: lookup handler in dispatch table
      const handler = dispatchTable[envelope.op];
      if (!handler) return;

      // Enrich meta with envelope.from so handlers can verify sender identity.
      // ONLY use envelope.from (the sender's declared identity), NOT meta.topic
      // (which is the inbox topic name like "set-a$inbox", not a sender identity).
      const enrichedMeta = { ...meta, from: envelope.from, correlationId: envelope.correlationId };

      // 5b. Contract-based payload validation (if actor declares static contract)
      const actorContract = (this.constructor as typeof MatrixActor).contract;
      if (actorContract && envelope.payload && typeof envelope.op === 'string') {
        const dotIdx = envelope.op.indexOf('.');
        if (dotIdx > 0) {
          const ifaceName = envelope.op.slice(0, dotIdx);
          const methodName = envelope.op.slice(dotIdx + 1);
          const iface = actorContract.interfaces[ifaceName];
          const opContract = iface?.ops[methodName];
          if (opContract?.payload) {
            const errors: string[] = [];
            const payloadRecord = envelope.payload as Record<string, unknown>;
            const replyEnvelope = payloadForRR as { replyTo?: string } | undefined;
            for (const [field, spec] of Object.entries(opContract.payload)) {
              if (spec.required && payloadRecord[field] === undefined) {
                errors.push(`missing required field: ${field}`);
              }
            }
            if (errors.length > 0) {
              // Reject with structured error — don't call handler
              const errorResponse = { ok: false, error: 'INVALID_PAYLOAD', details: errors };
              const replyTo = replyEnvelope?.replyTo;
              if (replyTo) {
                transport.publish(replyTo, errorResponse);
              }
              return;
            }
          }
        }
      }

      // 6. Direct mode: call with RequestReplyHelper (handles replyTo)
      try {
        await RequestReplyHelper.executeWithReplySafe(
          transport,
          payloadForRR,
          async (params) => handler.call(this, params ?? envelope.payload, enrichedMeta)
        );
        // Trigger shell DOM update after handler mutates state (data-bind)
        if (this._shellUpdateCallback) this._shellUpdateCallback();
      } catch (err) {
        // eslint-disable-next-line no-console -- dynamic handler failures need direct kernel diagnostics
        console.error(
          `[MatrixActor] Error in handler 'on${toHandlerCase(envelope.op)}':`,
          err
        );
      }
    };

    // ── NEW: Subscribe to $inbox (single topic, MxEnvelope format) ──

    const inboxTopic = TopicRouter.inbox(mount);
    const inboxUnsub = transport.subscribe(inboxTopic, async (payload, meta) => {
      const envelope = payload as MxEnvelope;
      if (!envelope || !envelope.op) return;
      await dispatchEnvelope(envelope, meta);
    }, { queue: matrixActorInboxQueue(mount) });
    this._subs.add(inboxUnsub);
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Private: Child event subscription (ONE implementation)
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Subscribe to events from a child actor.
   * Uses ComponentClass.emits to know what topics to subscribe to.
   *
   * Handler resolution order:
   * 1. Declarative binding from options.bindings (highest priority)
   * 2. Specific: onEventFromChildId
   * 3. Generic: onEvent
   */
  private _subscribeToChildEvents<T>(
    childId: string,
    childMount: string,
    ComponentClass: new () => T,
    _slot: string
  ): void {
    if (!this._context) return;

    const bindings = this._childBindings.get(childId);
    if (!bindings) return;

    // Get emits from ComponentClass.emits
    const CompClass = ComponentClass as IComponentClassWithTemplate;
    const emits = Object.keys(CompClass.emits ?? {});
    if (emits.length === 0) return;

    // Build dispatch table: event → handler
    const eventHandlers: Record<string, ComponentHandler> = {};

    for (const event of emits) {
      let handler: ComponentHandler | undefined;

      // 1. Declarative binding (highest priority)
      if (bindings.bindings && bindings.bindings[event]) {
        const boundHandlerName = bindings.bindings[event];
        const boundValue = this[boundHandlerName];
        if (typeof boundValue === 'function') {
          handler = boundValue as ComponentHandler;
        }
      }

      // 2. Specific: onEventFromChildId
      if (!handler) {
        const specificName = `on${toHandlerCase(event)}From${toHandlerCase(childId)}`;
        const specificValue = this[specificName];
        if (typeof specificValue === 'function') {
          handler = specificValue as ComponentHandler;
        }
      }

      // 3. Generic: onEvent
      if (!handler) {
        const genericName = `on${toHandlerCase(event)}`;
        const genericValue = this[genericName];
        if (typeof genericValue === 'function') {
          handler = genericValue as ComponentHandler;
        }
      }

      if (handler) {
        eventHandlers[event] = handler;
      }
    }

    if (Object.keys(eventHandlers).length === 0) return;

    // SINGLE subscription to child's $events topic
    const eventsTopic = TopicRouter.events(childMount);
    const off = this._context.transport.subscribe(eventsTopic, (payload, meta) => {
      const envelope = payload as MxEnvelope;
      if (!envelope || !envelope.op) return;
      const handler = eventHandlers[envelope.op];
      if (handler) {
        try {
          handler.call(this, envelope.payload, meta);
        } catch (err) {
          // eslint-disable-next-line no-console -- child event handler failures need direct kernel diagnostics
          console.error(
            `[MatrixActor] Error in child event handler for ${envelope.op} from ${childId}:`, err
          );
        }
      }
    });
    bindings.unsubs.add(off);
  }

  /**
   * Subscribe to child events using stored metadata (for remount).
   */
  private _subscribeToChildEventsFromMeta(id: string, mount: string, slot: string): void {
    const childInfo = this._childBindings.get(id) ?? { unsubs: new Set<() => void>() };

    const events = (this.constructor as typeof MatrixActor).childEventWhitelist
      ?? ['ready', 'disposed', 'error'];
    const eventSet = new Set(events);

    // SINGLE subscription to child's $events
    const eventsTopic = TopicRouter.events(mount);
    const unsub = this._context!.transport.subscribe(eventsTopic, (payload: unknown) => {
      const envelope = payload as MxEnvelope;
      if (!envelope || !envelope.op) return;
      if (eventSet.has(envelope.op)) {
        this.emit(`child.${envelope.op}`, { id, slot, payload: envelope.payload });
      }
    });
    childInfo.unsubs.add(unsub);

    this._childBindings.set(id, childInfo);
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Phase 2: Prompt Trigger Infrastructure
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Parse an interval string like '30s', '5m', '1h' into milliseconds.
   */
  private static _parseInterval(s: string): number {
    const match = s.match(/^(\d+)(s|m|h)$/);
    if (!match) return 0;
    const n = parseInt(match[1], 10);
    switch (match[2]) {
      case 's': return n * 1000;
      case 'm': return n * 60_000;
      case 'h': return n * 3_600_000;
      default: return 0;
    }
  }

  /**
   * Wire all declared promptTriggers. Called once after onBootstrap()
   * when promptable=true and promptTriggers is non-empty.
   */
  private _wirePromptTriggers(triggers: NonNullable<typeof MatrixActor.promptTriggers>): void {
    this._triggerSubs = new Map();
    this._triggerTimers = new Map();

    for (const trigger of triggers) {
      const pattern = trigger.event;
      if (!pattern) continue;

      if (pattern.startsWith('timer:')) {
        this._wireTimerTrigger(trigger);
      } else if (pattern.startsWith('child.')) {
        this._wireChildEventTrigger(trigger);
      } else if (pattern === '$stateChanged') {
        this._wireSelfStateTrigger(trigger);
      } else if (pattern.startsWith('fact.updated:')) {
        this._wireFactTrigger(trigger);
      }
    }
  }

  /**
   * Timer trigger — registers with master's SchedulerActor (scheduler.create).
   * The scheduler fires the specified op on this actor's $inbox at each interval.
   * We use a synthetic op '$trigger.timer' that routes to _handleTriggerFire.
   */
  private _wireTimerTrigger(trigger: NonNullable<typeof MatrixActor.promptTriggers>[0]): void {
    if (!this._context) return;
    const intervalStr = trigger.event.replace('timer:', '');
    const intervalMs = MatrixActor._parseInterval(intervalStr);
    if (!intervalMs) return;

    // Register with scheduler — it fires '$trigger.timer' on our $inbox
    const scheduleId = `trig-${this._context.mount}-${trigger.event}`;
    RequestReply.sendToInbox(this._context, 'system.scheduler', 'scheduler.create', {
      id: scheduleId,
      mount: this._context.mount,
      op: '$trigger.timer',
      payload: { triggerId: trigger.event },
      intervalMs,
    });

    // Subscribe to our own $inbox for $trigger.timer op
    // (The scheduler calls RequestReply.execute on us, dispatching to on$TriggerTimer)
    // We register a no-op unsub since the scheduler manages the timer lifecycle.
    // The actual handling happens in the handler below.
    this._triggerSubs!.set(trigger.event, {
      event: trigger.event,
      unsub: () => {
        // Cancel the schedule when this trigger is disposed
        RequestReply.sendToInbox(this._context!, 'system.scheduler', 'scheduler.cancel', {
          scheduleId,
        });
      },
      lastFiredAt: 0, fireCount: 0,
      hourBoundary: Math.floor(Date.now() / 3_600_000),
      maxPerHour: trigger.maxPerHour ?? 60,
    });
  }

  /**
   * Handler for scheduler-dispatched timer ticks.
   * SchedulerActor calls RequestReply.execute(mount, '$trigger.timer', payload).
   */
  on$TriggerTimer(payload: Record<string, unknown>): void {
    const triggerId = typeof payload.triggerId === 'string' ? payload.triggerId : '';
    const ctor = this.constructor as typeof MatrixActor;
    const trigger = this._runtimePromptTriggers?.get(triggerId)
      ?? ctor.promptTriggers?.find(t => t.event === triggerId);
    if (trigger) {
      this._handleTriggerFire(trigger, payload);
    }
  }

  /**
   * Child event trigger — subscribes to children's $events topic.
   * Patterns: 'child.error', 'child.*', 'child.{id}.error'
   */
  private _wireChildEventTrigger(trigger: NonNullable<typeof MatrixActor.promptTriggers>[0]): void {
    if (!this._context) return;
    const parts = trigger.event.split('.');
    // child.error → childId='*', eventName='error'
    // child.*     → childId='*', eventName='*'
    // child.{id}.error → childId='{id}', eventName='error'
    const childId = parts.length === 3 ? parts[1] : '*';
    const eventName = parts.length === 3 ? parts[2] : parts[1];

    // Subscribe to all children's $events
    // TopicRouter.events(mount) returns '{mount}/$events'
    // For wildcard child subscription, we use '{mount}.*/$events'
    const eventsTopic = `${this._context.mount}.*/$events`;
    const unsub = this._context.transport.subscribe(eventsTopic, (payload: unknown, meta?: { topic?: string }) => {
      const envelope = payload as MxEnvelope;
      if (!envelope?.op) return;

      // Extract child segment from topic: '{mount}.{child}/$events'
      const topic = meta?.topic ?? '';
      const prefix = `${this._context!.mount}.`;
      const suffix = `/$events`;
      if (!topic.startsWith(prefix) || !topic.endsWith(suffix)) return;
      const childSegment = topic.slice(prefix.length, topic.length - suffix.length);

      // Match child pattern
      if (childId !== '*' && childSegment !== childId) return;
      // Match event pattern
      if (eventName !== '*' && envelope.op !== eventName) return;

      this._handleTriggerFire(trigger, envelope.payload);
    });

    this._triggerSubs!.set(trigger.event, {
      event: trigger.event, unsub,
      lastFiredAt: 0, fireCount: 0,
      hourBoundary: Math.floor(Date.now() / 3_600_000),
      maxPerHour: trigger.maxPerHour ?? 60,
    });
  }

  /**
   * Self state trigger — fires when this actor's state changes.
   */
  private _wireSelfStateTrigger(trigger: NonNullable<typeof MatrixActor.promptTriggers>[0]): void {
    if (!this._context) return;
    const eventsTopic = TopicRouter.events(this._context.mount);
    const unsub = this._context.transport.subscribe(eventsTopic, (payload: unknown) => {
      const envelope = payload as MxEnvelope;
      if (envelope?.op === '$stateChanged') {
        this._handleTriggerFire(trigger, envelope.payload);
      }
    });

    this._triggerSubs!.set(trigger.event, {
      event: trigger.event, unsub,
      lastFiredAt: 0, fireCount: 0,
      hourBoundary: Math.floor(Date.now() / 3_600_000),
      maxPerHour: trigger.maxPerHour ?? 60,
    });
  }

  /**
   * Fact trigger — fires when a blackboard fact changes.
   * Pattern: 'fact.updated:{domain}'
   */
  private _wireFactTrigger(trigger: NonNullable<typeof MatrixActor.promptTriggers>[0]): void {
    if (!this._context) return;
    const domain = trigger.event.replace('fact.updated:', '');
    const eventsTopic = TopicRouter.events('system.blackboard');

    const unsub = this._context.transport.subscribe(eventsTopic, (payload: unknown) => {
      const envelope = payload as MxEnvelope;
      if (envelope?.op === 'fact.updated') {
        const factDomain = (envelope.payload as Record<string, unknown>)?.domain;
        if (domain === '*' || factDomain === domain) {
          this._handleTriggerFire(trigger, envelope.payload);
        }
      }
    });

    this._triggerSubs!.set(trigger.event, {
      event: trigger.event, unsub,
      lastFiredAt: 0, fireCount: 0,
      hourBoundary: Math.floor(Date.now() / 3_600_000),
      maxPerHour: trigger.maxPerHour ?? 60,
    });
  }

  /**
   * Central trigger fire handler — condition eval, budget check, debounce, dispatch.
   */
  private _handleTriggerFire(
    trigger: NonNullable<typeof MatrixActor.promptTriggers>[0],
    eventPayload?: unknown,
  ): void {
    const key = trigger.event;
    const sub = this._triggerSubs?.get(key);
    if (!sub || !this._context) return;

    // Condition check — gated behind SecurityRealm code evaluation capability
    if (trigger.condition && eventPayload != null) {
      const securityRealm = this._context.getService<{ canEvaluateCode?: () => boolean }>('security-realm');
      if (securityRealm?.canEvaluateCode?.() !== false) {
        // Permitted: evaluate condition (same pattern as factory.create-from-code)
        try {
          const fn = new Function('payload', `return !!(${trigger.condition})`);
          if (!fn(eventPayload)) return;
        } catch {
          return; // Malformed condition = skip
        }
      }
      // If security realm denies code eval, always-fire (condition skipped)
    }

    // Budget check (reset on hour boundary)
    const now = Date.now();
    const currentHour = Math.floor(now / 3_600_000);
    if (currentHour !== sub.hourBoundary) {
      sub.fireCount = 0;
      sub.hourBoundary = currentHour;
    }
    if (sub.fireCount >= sub.maxPerHour) {
      return; // Budget exhausted
    }

    // Debounce check
    const debounceMs = trigger.debounceMs ?? 5000;
    if (debounceMs > 0) {
      const existing = this._triggerTimers?.get(key);
      if (existing) clearTimeout(existing);

      const elapsed = now - sub.lastFiredAt;
      if (elapsed < debounceMs) {
        const timer = setTimeout(() => {
          this._triggerTimers?.delete(key);
          this._executeTriggerPrompt(trigger, sub, eventPayload);
        }, debounceMs - elapsed);
        this._triggerTimers?.set(key, timer);
        return;
      }
    }

    this._executeTriggerPrompt(trigger, sub, eventPayload);
  }

  /**
   * Execute the actual self-prompt via system.agents.
   * Follows the same path as on$prompt() — full trace chain (C9).
   */
  private _executeTriggerPrompt(
    trigger: NonNullable<typeof MatrixActor.promptTriggers>[0],
    sub: { lastFiredAt: number; fireCount: number },
    eventPayload?: unknown,
  ): void {
    if (!this._context) return;
    const ctor = this.constructor as typeof MatrixActor;
    const mount = this._context.mount;

    // Update tracking
    sub.lastFiredAt = Date.now();
    sub.fireCount++;

    // Build prompt
    const promptText = trigger.prompt || ctor.purpose || 'Reason about the triggered event.';
    const eventContext = eventPayload != null
      ? `\n\n[Trigger: ${trigger.event}]\nEvent: ${JSON.stringify(eventPayload)}`
      : `\n\n[Trigger: ${trigger.event}]`;

    // C9: trace chain for self-prompts
    const traceId = `trig-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    const requestId = `trig-${trigger.event}-${Date.now()}`;

    // Resolve LLM endpoint (respects service cascade — Pattern 2)
    const llmTarget = this._context.getService<string>('llm-endpoint') ?? 'system.agents';

    // Build snapshot tree (minimal — enough for oracle context)
    const snapshotTree = {
      mount,
      tag: ctor.name,
      accepts: Object.keys(ctor.accepts ?? {}),
      purpose: ctor.purpose,
    };

    const transport = this._context.transport as unknown as { root?: string };
    const callerRoot = transport?.root ?? 'unknown';

    // Fire-and-forget to daemon (same pipeline as user prompts)
    RequestReply.sendToInbox(this._context, llmTarget, '$prompt', {
      prompt: promptText + eventContext,
      requestId,
      traceId,
      callerRoot,
      callerMount: mount,
      snapshotTree,
      targetSnapshots: [snapshotTree],
      triggerSource: trigger.event,
    });
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Private: Child monitoring
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Subscribe to exit signals from a child actor.
   */
  private _monitorChild(child: { id: string; mount: string; unsubs: Set<() => void> }): void {
    if (!this._context) return;

    const exitTopic = `${child.mount}.$exit`;
    const unsub = this._context.transport.subscribe(exitTopic, (payload) => {
      const signal = payload as IExitSignal;
      this.handleChildExit(signal).catch(err => {
        // eslint-disable-next-line no-console -- async exit handling failures need direct kernel diagnostics
        console.error(`[MatrixActor] Error handling exit signal:`, err);
      });
    });

    child.unsubs.add(unsub);
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Private: Remount children
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Remount all children with new derived contexts.
   */
  protected async _remountChildren(): Promise<void> {
    if (!this._context || !this._children) return;

    for (const child of this._children.getAllChildren()) {
      const { id, mount } = child;
      const bindings = this._childBindings.get(id);

      // Unsubscribe parent's old child event subscriptions
      if (bindings) {
        for (const unsub of bindings.unsubs) {
          try { unsub(); } catch { /* ignore */ }
        }
        bindings.unsubs.clear();
      }

      // Derive new context
      const newChildContext = this._context.derive(id);

      // Send rebind command via $inbox
      this._context.transport.publish(
        TopicRouter.inbox(mount),
        { op: '$rebind', payload: { parentMount: this.mount, newMount: newChildContext.mount } } as MxEnvelope
      );

      // Re-subscribe to child events
      this._subscribeToChildEventsFromMeta(id, newChildContext.mount, child.slot);
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Private: Unbind all subscriptions
  // ═══════════════════════════════════════════════════════════════════════

  private _unbindAll(): void {
    for (const unsub of this._subs) {
      unsub();
    }
    this._subs.clear();
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Private: Lamport clock
  // ═══════════════════════════════════════════════════════════════════════

  private _toSerializable(value: unknown): unknown {
    try {
      return JSON.parse(JSON.stringify(value));
    } catch {
      if (value instanceof Error) {
        return { name: value.name, message: value.message };
      }
      return String(value);
    }
  }

  // ═══════════════════════════════════════════════════════════
  // Membrane methods (Ring 0 dispatch interception)
  // ═══════════════════════════════════════════════════════════

  /** Initialize the default membrane stepper. Called lazily on first need. */
  protected _initDefaultMembrane(): void {
    this._membrane = MembraneStepper.create();
    this._localMembrane = this._membrane.compile(DEFAULT_MEMBRANE_SOURCE);
    this._localMembraneSource = DEFAULT_MEMBRANE_SOURCE;
    // Mark as default for fast-path (skips CESK evaluation entirely)
    this._membrane.setDefaultProgram(this._localMembrane);
  }

  /** Wall-clock timeout for membrane execution. Fail-open (allow) on timeout. */
  private static MEMBRANE_TIMEOUT_MS = 50;

  /**
   * Run the membrane program(s) for a message with timeout protection.
   * Fail-open: if the membrane hangs or errors, the message is allowed through.
   */
  private async _runMembrane(
    envelope: MxEnvelope,
    meta: { topic: string; ts: number },
  ): Promise<MembraneResult> {
    try {
      return await Promise.race([
        this._runMembraneInner(envelope, meta),
        new Promise<MembraneResult>((_, reject) =>
          setTimeout(() => reject(new Error('Membrane timeout')), MatrixActor.MEMBRANE_TIMEOUT_MS)
        ),
      ]);
    } catch {
      // Timeout or unexpected error — fail-open to prevent bricking
      return { action: 'allow' };
    }
  }

  /** Inner membrane execution — inherited first, then local. */
  private async _runMembraneInner(
    envelope: MxEnvelope,
    meta: { topic: string; ts: number },
  ): Promise<MembraneResult> {
    if (!this._membrane) return { action: 'allow' };

    const t0 = performance.now();

    const bindings = {
      op: envelope.op,
      payload: envelope.payload,
      sender: envelope.from || '',
      meta: { topic: meta.topic, ts: meta.ts, correlationId: envelope.correlationId, replyTo: envelope.replyTo },
    };

    // Track accumulated payload mutations across zones
    let mutatedPayload: unknown = undefined;
    let hasMutation = false;

    // Run inherited membranes in order (ancestors → parent)
    for (const inherited of this._inheritedMembranes) {
      const inheritedResult = await this._membrane.run(inherited, bindings);
      if (inheritedResult.action === 'deny') {
        this._warnSlowMembrane(t0, envelope.op);
        return inheritedResult;
      }
      if (inheritedResult.action === 'mutate') {
        bindings.payload = inheritedResult.payload;
        mutatedPayload = inheritedResult.payload;
        hasMutation = true;
      }
    }

    // Run local membrane (if set)
    if (this._localMembrane) {
      const localResult = await this._membrane.run(this._localMembrane, bindings);
      this._warnSlowMembrane(t0, envelope.op);
      if (localResult.action === 'deny') return localResult;
      if (localResult.action === 'mutate') {
        // Compose: merge inherited mutations with local mutations.
        // Only shallow-merge when BOTH payloads are plain objects. If either is
        // a scalar/array, the local mutation wins (no silent data corruption).
        if (hasMutation) {
          if (isMergeablePayload(mutatedPayload) && isMergeablePayload(localResult.payload)) {
            return { action: 'mutate', payload: { ...mutatedPayload, ...localResult.payload } };
          }
          // Non-object mutation: local wins (inherited scalar/array is dropped)
        }
        return localResult;
      }
      // Local allows — but if earlier zone mutated, carry the mutation forward
      if (hasMutation) {
        return { action: 'mutate', payload: mutatedPayload };
      }
      return localResult;
    }

    this._warnSlowMembrane(t0, envelope.op);

    // No local membrane — if inherited mutated, return that
    if (hasMutation) {
      return { action: 'mutate', payload: mutatedPayload };
    }
    return { action: 'allow' };
  }

  /** Derive the parent's mount path from this actor's mount (e.g. 'a.b.c' → 'a.b'). */
  private _deriveParentMount(): string {
    const m = this.mount;
    const lastDot = m.lastIndexOf('.');
    return lastDot > 0 ? m.substring(0, lastDot) : '';
  }

  private _warnSlowMembrane(t0: number, op: string): void {
    const elapsed = performance.now() - t0;
    if (elapsed > 5) {
      // eslint-disable-next-line no-console -- membrane performance guardrail logs stay in kernel layer
      console.warn(`[Membrane] Slow membrane at ${this.mount}: ${elapsed.toFixed(2)}ms for op=${op}`);
    }
  }

  /** Set a custom local membrane program (Lisp source). */
  setLocalMembrane(lispSource: string): void {
    if (!this._membrane) {
      this._membrane = MembraneStepper.create();
    }
    this._localMembrane = this._membrane.compile(lispSource);
    this._localMembraneSource = lispSource;
  }

  /** Clear the local membrane (revert to inherited-only or no membrane). */
  clearLocalMembrane(): void {
    this._localMembrane = null;
    this._localMembraneSource = null;
  }

  /** Get the membrane stepper (for testing). */
  getMembrane(): MembraneStepper | null {
    return this._membrane;
  }

  /**
   * Get the membrane source array that should be inherited by children.
   * Collects parent's inherited sources + parent's local source.
   * Returns empty array if no membrane is active.
   */
  _getInheritedSourcesForChildren(): string[] {
    const sources: string[] = [...this._inheritedMembraneSources];
    if (this._localMembraneSource) {
      sources.push(this._localMembraneSource);
    }
    return sources;
  }

  /**
   * Propagate the current membrane to a child via $membrane-inherit message.
   * Called after addChild/on$join when parent has active membrane programs.
   */
  protected _propagateMembraneToChild(childMount: string): void {
    const sources = this._getInheritedSourcesForChildren();
    if (sources.length === 0 || !this._context) return;
    this._context.transport.publish(TopicRouter.inbox(childMount), {
      op: '$membrane-inherit',
      payload: { sources },
      from: this.mount,
    } as MxEnvelope);
  }

  private _pushBoundedHistory<T>(buffer: T[], item: T): void {
    buffer.push(item);
    if (buffer.length > this._historyLimit) {
      buffer.splice(0, buffer.length - this._historyLimit);
    }
  }

  private _attachLamportClock(payload: unknown, lamportClock: number): unknown {
    if (payload === null || payload === undefined) {
      return { __lamportClock: lamportClock };
    }
    if (typeof payload === 'object' && !Array.isArray(payload)) {
      return { ...payload as object, __lamportClock: lamportClock };
    }
    return { __value: payload, __lamportClock: lamportClock };
  }

  private _receiveLamportClock(payload: unknown): void {
    if (typeof payload === 'object' && payload !== null) {
      const typedPayload = payload as Record<string, unknown>;
      if (typeof typedPayload.__lamportClock === 'number') {
        this._lamportClock.receive(typedPayload.__lamportClock);
      }
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Private: Dynamic component compilation
  // ═══════════════════════════════════════════════════════════════════════

  private _compileDynamicComponent(
    code: string, template?: string, styles?: string
  ): new () => MatrixActor {
    if (!this._context?.runtime.isDynamicCompilationAllowed?.()) {
      throw new Error(
        '[MatrixActor] Dynamic component compilation is DISABLED in this security policy.'
      );
    }

    try {
      rejectKnownBadPatterns(code);

      const factory = new Function(
        'MatrixActor', `"use strict"; ${code}`
      ) as (base: typeof MatrixActor) => new () => MatrixActor;

      const ComponentClass = factory(MatrixActor);

      if (!ComponentClass || typeof ComponentClass !== 'function') {
        throw new Error('Code did not return a valid class');
      }

      const templatedComponentClass = ComponentClass as TemplateComponentClass;
      if (template && !templatedComponentClass.template) {
        const fullTemplate = styles ? `${template}<style>${styles}</style>` : template;
        templatedComponentClass.template = fullTemplate;
      } else if (styles && !templatedComponentClass.template) {
        templatedComponentClass.template = `<style>${styles}</style>`;
      }

      return ComponentClass;
    } catch (err: unknown) {
      if (err instanceof SyntaxError) {
        throw new Error(`Syntax error in component code: ${err.message}`);
      }
      throw err;
    }
  }

  private _hashCode(str: string): string {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash;
    }
    return Math.abs(hash).toString(36);
  }
}
