/** * HookSurface — tri-partition hook installer for the OpenClaw gateway. * * The OpenClaw runtime has three distinct hook dispatch paths that never * cross (plan §2.7 / A3): * * - `'typed'` kind — `before_prompt_build`, `agent_end`, `before_compaction`, * `before_reset`. Dispatched via `registry.typedHooks`, * populated ONLY by `api.on` and mode-gated to `full` * at `registry.ts:1150`. * * - `'internal'` kind — `message:received`, `message:sent`. Dispatched by the * runtime into `globalThis[Symbol.for("openclaw.internalHookHandlers")]`. * Mode-independent. The PR #216 mechanism. Not a * fallback for typed hooks. * * - `'legacy'` kind — `session_end` and other pre-typed-hook names. * `api.registerHook` pushes to `registry.hooks`. * Mode-independent. * * `install(kind, event, handler)` picks exactly ONE path per `(kind, event)` * pair per the strategy table. No cross-class fallback: if `api.on` is * absent for a `'typed'` hook, install returns `null` and callers log a * loud warn. Using `api.registerHook` for a typed event would silently land * in the legacy registry and never dispatch. * * Kill-switch `strategyOverride`: * - `'auto'` (default) — use the table. * - `'api-on'` — force `api.on` path for `'typed'` only. T50 — * legacy installs continue to use `api.registerHook` * regardless of the override, because legacy events * dispatch from `registry.hooks` (not `typedHooks`), * so an api.on install for a legacy event would * silently never fire. For `'internal'` kind, warn * and fall back to globalThis (N9 footgun guard) — * internal events are dispatched by the runtime * into the globalThis map, not the typed-hook * dispatcher. * - `'off'` — skip all installs, return `null` from every call. * Emergency kill switch for prod gateway surprises. * * I4 — deterministic commit timing. After first observed fire OR a 30s * grace period (whichever first), each event's `commitState` flips to * `committed-by-fire`, `committed-by-peer-fire`, or * `committed-by-timeout`. Timeout is diagnostic only: a hook can fire later, * at which point stats move to `committed-by-fire`. * * C5 — double-registration guard. The same `(kind, event, handler)` triple * is a no-op on repeat install; we return the existing unsubscribe. * * Never throws. All failures are recorded in `getDispatchStats()`. */ import type { OpenClawPluginApi } from './types.js'; export type HookKind = 'typed' | 'internal' | 'legacy'; export type HookStrategy = 'auto' | 'api-on' | 'off'; export type HookHandler = (...args: any[]) => unknown | Promise; export type Unsubscribe = () => void; /** Symbol the gateway uses to expose the internal hook registry. */ export declare const INTERNAL_HOOK_SYMBOL: unique symbol; /** Which surface actually received the install, if any. */ export type InstalledVia = 'on' | 'registerHook' | 'globalThis' | 'none'; /** Commit state per I4 — frozen after first fire or 30s grace. */ export type CommitState = 'pending' | 'committed-by-fire' | 'committed-by-peer-fire' | 'committed-by-timeout'; export interface DispatchStats { installedVia: InstalledVia; fireCount: number; commitState: CommitState; installError?: string; } export interface HookInstallOptions { rareFireExpected?: boolean; /** * Multi-surface typed-hook installs can race: OpenClaw may dispatch a * typed event through one retained API registry while sibling registries * stay idle. When a sibling has observed the same event since this install * began, this install is proven live enough for the process and should not * emit a duplicate timeout warning. */ observedFireSinceInstall?: () => boolean; } /** Minimum logger shape used by HookSurface. */ export interface HookSurfaceLogger { info?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void; debug?: (...args: unknown[]) => void; } export declare class HookSurface { private readonly api; private readonly logger; private readonly strategyOverride; private readonly commitGraceMs; /** * Per-event stats. Keyed on `${kind}:${event}` so the same event name * registered under two different kinds stays separate. */ private readonly stats; /** * Double-registration guard (C5). Maps `${kind}:${event}` to the * `{ handler, unsubscribe }` tuple. Repeat installs with the same * handler identity return the existing unsubscribe; different-handler * installs against the same event are rejected with a warn — we want * exactly one handler per surface slot to keep dispatch observable. */ private readonly installedHandlers; /** * Internal hooks are stored in a mutable process-global map owned by the * gateway. Stats only tell us an install once succeeded; this map lets the * adapter prove its own wrapper is still present in the current live map. */ private readonly internalWrappedHandlers; /** * R21.1 — Soft "destroyed" flag. OpenClaw's `api.on` and `api.registerHook` * have no unsubscribe primitives, so `destroy()`'s no-op unsubscribes for * typed and legacy hooks leave handlers live in the upstream registry. * Each wrapped handler checks this flag and short-circuits BEFORE * invoking the user handler when the surface has been torn down. Without * this gate, `before_prompt_build` / `agent_end` / `session_end` would * keep firing the old plugin's logic after `destroy()` returned. */ private destroyed; /** Timers for the I4 grace-period commit path. Cleared on first fire or destroy. */ private readonly commitTimers; private readonly rareFireKeys; constructor(api: OpenClawPluginApi, logger: HookSurfaceLogger, strategyOverride?: HookStrategy, opts?: { commitGraceMs?: number; }); /** * Install a handler for a `(kind, event)` pair. * Returns an unsubscribe callback, or `null` when the install failed * (e.g. `api.on` absent for a typed hook). Never throws. * * `opts.rareFireExpected` marks hooks that normally don't fire during * routine traffic (e.g. `before_compaction`, `before_reset`). Their * 30s commit-by-timeout message downgrades to debug instead of warn — * a healthy startup otherwise surfaces noise warnings that drown out * real install failures. */ install(kind: HookKind, event: string, handler: HookHandler, opts?: HookInstallOptions): Unsubscribe | null; /** * Read-only snapshot of per-event dispatch stats. Keys are `${kind}:${event}`. */ getDispatchStats(): Record; /** * True only when this surface's adapter-owned wrapper for `event` is still * present in the current process-global internal hook map. */ ownsCurrentInternalHook(event: string): boolean; /** * Tear down all installed handlers and cancel pending commit timers. * Idempotent. Called from `DkgNodePlugin.stop()` via the existing * `session_end` legacy hook. */ destroy(): void; private installTyped; private installInternal; private installLegacy; private setStat; private recordFire; private clearCommitTimer; } //# sourceMappingURL=HookSurface.d.ts.map