import type { MoltbotPluginAPI, PluginConfig, PluginHookAgentContext, MemoryResult, RetainRequest } from "./types.js"; import { HindsightClient, type HindsightClientOptions } from "@vectorize-io/hindsight-client"; import type { Route } from "./router/types.js"; import type { RecallResponse } from "./types.js"; export interface BankScopedClient { readonly bankId: string; retain(req: RetainRequest): Promise; recall(req: { query: string; maxTokens?: number; budget?: "low" | "mid" | "high"; types?: Array<"world" | "experience" | "observation">; /** * When true, asks Hindsight to include the full retrieval trace * (per-candidate cross_encoder_score, recency, temporal, proof_count, * combined_score) in `response.trace.final_results`. The plugin's * `recallMinRelevance` filter relies on this trace. */ trace?: boolean; }, timeoutMs?: number): Promise; setMission(mission: string): Promise; } /** * Wrap a `HindsightClient` into a `BankScopedClient` that targets a * single bank. Exported (since 0.8.0) so unit tests can directly verify * the wrapper propagates options like `trace` to the underlying SDK * without going through the full plugin hook chain. Advisor 2026-05-24 * pass-4 fix: this propagation used to be untested because the * integration test stub bypasses the factory entirely. * * @internal exported for unit testing */ export declare function scopeClient(c: HindsightClient, bankId: string): BankScopedClient; export type IdentitySkipReason = { kind: "retryable"; detail: "missing stable message provider" | "missing stable sender identity"; } | { kind: "final"; detail: string; }; /** * Strip plugin-injected memory tags from content to prevent retain feedback loop. * Removes and blocks that were injected * during before_agent_start so they don't get re-stored into the memory bank. */ export declare function stripMemoryTags(content: string): string; /** * Extract per-message retain tag overrides from inline user content. * * Supported forms: * - tag:a, tag:b * - tag:a, tag:b */ export declare function extractInlineRetainTags(content: string): string[]; /** * Remove inline retain tag directives from message content before storing it. */ export declare function stripInlineRetainTags(content: string): string; export declare function stripInlineTimestampPrefix(content: string): string; /** * Extract sender_id from OpenClaw's injected inbound metadata blocks. * Checks both "Conversation info (untrusted metadata)" and "Sender (untrusted metadata)" blocks. * Returns the first sender_id / id string found, or undefined if none. */ export declare function extractSenderIdFromText(text: string): string | undefined; /** * Strip OpenClaw sender/conversation metadata envelopes from message content. * These blocks are injected by OpenClaw but are noise for memory storage and recall. */ export declare function stripMetadataEnvelopes(content: string): string; /** * Extract a recall query from a hook event's rawMessage or prompt. * * Prefers rawMessage (clean user text). Falls back to prompt, stripping * envelope formatting (System: lines, [Channel ...] headers, [from: X] footers). * * Returns null when no usable query (< 5 chars) can be extracted. */ export declare function extractRecallQuery(rawMessage: string | undefined, prompt: string | undefined): string | null; export declare function composeRecallQuery(latestQuery: string, messages: any[] | undefined, recallContextTurns: number, recallRoles?: Array<"user" | "assistant" | "system" | "tool">): string; export declare function truncateRecallQuery(query: string, latestQuery: string, maxChars: number): string; export interface ParsedSessionKey { agentId?: string; provider?: string; channel?: string; } export declare function parseSessionKey(sessionKey: string): ParsedSessionKey; export declare function extractTelegramDirectSenderId(channelId: string | undefined): string | undefined; /** * Extract the canonical user identifier from a direct-channel segment. * * When OpenClaw's `session.identityLinks` maps a provider peer to a canonical * name (e.g. `{ alice: ["telegram:123", "discord:987"] }`), the sessionKey * becomes `agent:::direct:alice` regardless of the source * channel. Parsing this segment gives us the unified human identity, which * lets Hindsight consolidate memory across channels for the same person. * * Returns undefined when no `direct:` segment is present (group channels, * cron/heartbeat/subagent triggers, or unresolved identities). */ export declare function extractCanonicalDirectUser(channel: string | undefined): string | undefined; export declare function resolveSessionIdentity(ctx: PluginHookAgentContext | undefined): PluginHookAgentContext | undefined; export declare function getIdentitySkipReason(ctx: PluginHookAgentContext | undefined, pluginConfig?: PluginConfig): { resolvedCtx: PluginHookAgentContext | undefined; reason?: IdentitySkipReason; }; export declare function isEphemeralOperationalText(text: string | undefined): boolean; /** * Extract the most recent user-role text from a list of session messages. * * Used by the retain hook to apply `isEphemeralOperationalText` on the raw * user prompt BEFORE the messages are serialized to JSON. The 0.8.0 * introspection patterns are anchored on `^...$`, so they cannot match * once the content is wrapped in a JSON array — pre-extracting the text * keeps the filter effective regardless of `retainFormat`. * * Handles both flat string content and Anthropic-style structured content * blocks (`[{type: "text", text: "..."}]`). Returns `null` when no user * message has usable text — the caller treats that as "no filter possible". * * @internal exported for unit testing */ export declare function extractLatestUserText(messages: any[]): string | null; /** * Sender IDs that indicate the OpenClaw gateway could not authenticate the * principal and fell back to a channel-name placeholder. Sessions with these * senderIds would otherwise create phantom memory banks like * `prefix-agent::openclaw-control-ui` for unauthenticated traffic * (e.g. internal Playwright browser sessions launched by the `browser` skill, * health checks, or app devices that lost their gateway token). * * Used as the default value of `excludeSenders` when the option is unset. * * Note: the default list is intentionally NOT eagerly materialized into * `getPluginConfig` (unlike `excludeProviders`). Keeping the fallback lazy * here lets `isExcludedSender` distinguish "operator left the option unset" * (use defaults) from "operator passed `[]`" (opt-out). The exported * constant remains the single source of truth for the default policy. */ export declare const DEFAULT_EXCLUDED_SENDERS: readonly string[]; /** * Returns true when the resolved senderId indicates a session that should * not write to the memory store. Callers in the recall/retain hooks use * this to short-circuit before any bank is created or written to. * * Missing senderIds (undefined / empty string) are normalized to the * sentinel "anonymous" before lookup. Note that the upstream * `getIdentitySkipReason` already rejects falsy senderIds and the literal * "anonymous" before this function is called, so the normalization is * belt-and-suspenders rather than the closure of an active hole. It buys * insurance against a future refactor that relaxes the upstream guard, * and keeps the three policy regimes uniform: * - default config (which excludes "anonymous") would block them; * - explicit opt-out (`excludeSenders: []`) would let them through, * preserving the documented "disable the protection" semantics; * - custom lists give operators full control (omit "anonymous" at your * own risk — the normalization no longer catches anything). */ export declare function isExcludedSender(senderId: string | undefined, pluginConfig: PluginConfig): boolean; /** * Compute the value that `deriveBankId` will use as the `user` segment. * * Mirrors the resolution chain at the bottom of `deriveBankId`: * canonicalDirectUser (from sessionKey `direct:`) -> senderId -> "anonymous" * * Exposed so the recall/retain hooks can apply `excludeSenders` against the * value that will actually flow into the bank ID — not against the raw * `senderId`. This is the fix for the 0.7.6 regression where legitimate * human sessions with a resolved canonical identity but a fallback * `senderId="openclaw-control-ui"` were skipped, even though `deriveBankId` * would have routed them to a legitimate `{prefix}-{agent}::{canonical}` * bank. */ export declare function resolveBankUserSegment(ctx: PluginHookAgentContext | undefined): string; /** * Returns true when the configured bank ID format will include a `user` * segment derived from the agent context. The `excludeSenders` guard is * only meaningful when this is true — in static-bank mode, or when * `user` is not part of `dynamicBankGranularity`, `deriveBankId` * produces an id that does not depend on the resolved sender at all, * so a "phantom bank" shape simply cannot occur and applying the guard * would over-block legitimate sessions. */ export declare function bankIdExposesUserSegment(pluginConfig: PluginConfig): boolean; /** * Derive a bank ID from the agent context. * Uses configurable dynamicBankGranularity to determine bank segmentation. * Falls back to default bank when context is unavailable. */ export declare function deriveBankId(ctx: PluginHookAgentContext | undefined, pluginConfig: PluginConfig): string; export declare function formatMemories(results: MemoryResult[]): string; /** * Narrow the configured recall types according to a router decision. * * - `ALL` → preserves the operator-configured `recallTypes`. * - `WORLD_ONLY` → keeps `"world"` (and `"observation"` when * `observationFollowsNarrow` is true and observation is in baseTypes). * - `EXPERIENCE_ONLY` → keeps `"experience"` (and `"observation"` * under the same condition). * - `NONE` → never reaches this function (the hook short-circuits earlier); * defensive fallback returns baseTypes unchanged. * * `observationFollowsNarrow` (default true) controls whether * consolidated `observation` facts are preserved alongside narrow * routes. Observations are derived from world/experience and are often * the highest-signal recall targets, so dropping them on narrow routes * would silently lose the most relevant fact for many queries. Set to * false for strict route exclusivity. * * Returns `baseTypes` unchanged when the requested narrowing would yield an * empty list — the operator's broader configuration wins over a router that * would otherwise silently drop all retrieval for the turn. Fail-open. * * @internal exported for unit testing */ export declare function narrowTypesByRoute(baseTypes: Array<"world" | "experience" | "observation">, route: Route, observationFollowsNarrow?: boolean): Array<"world" | "experience" | "observation">; /** * Build a `Map` from a `RecallResponse.trace` * payload. The shape of `trace.final_results` is documented in the * Hindsight `reranking.py` source: an array of `{ id, cross_encoder_score, * combined_score, ... }`. Several historical names for the score field are * accepted defensively (`cross_encoder_score`, `cross_encoder_normalized`, * `scores.cross_encoder`) so a server-side rename does not silently break * the threshold filter. * * Returns an empty map when the trace is missing, malformed, or contains * no recognizable cross_encoder field — the caller must treat this as * "filter unavailable" and fail-open. * * @internal exported for unit testing */ export declare function extractCrossEncoderScores(trace: unknown): Map; export declare function detectLLMConfig(pluginConfig?: PluginConfig): { provider?: string; apiKey?: string; model?: string; baseUrl?: string; source: string; }; /** * Detect external Hindsight API configuration from plugin config. */ export declare function detectExternalApi(pluginConfig?: PluginConfig): { apiUrl: string | null; apiToken: string | null; }; /** * Build HindsightClientOptions for the generated hindsight-client. In * external-API mode we use the configured URL/token; in local daemon mode * the caller overrides with the daemon's base URL after start(). * The llmConfig parameter is currently only consumed by the daemon manager * (via env vars); it's kept on the client builder signature so callers * don't need to branch and so future features can forward it. */ export declare function buildClientOptions(_llmConfig: { provider?: string; apiKey?: string; model?: string; }, _pluginCfg: PluginConfig, externalApi: { apiUrl: string | null; apiToken: string | null; }): HindsightClientOptions; /** * Health check for external Hindsight API. * Retries up to 3 times with 2s delay — container DNS may not be ready on first boot. */ /** * Compare two semver-shaped strings ("0.5.0", "0.4.22"). Returns true when * `actual >= minimum`. Tolerates pre-release suffixes (treats them as the * same major.minor.patch as the bare version — good enough for capability * gating). */ export declare function meetsMinimumVersion(actual: string, minimum: string): boolean; export declare function normalizeRetainTags(value: unknown): string[]; export declare function getPluginConfig(api: MoltbotPluginAPI): PluginConfig; /** Default threshold applied when `recallMinRelevance` is absent from the config. */ export declare const DEFAULT_RECALL_MIN_RELEVANCE = 0.3; /** * Parse the raw `recallMinRelevance` config value. * * Contract: * - `undefined` (key absent) → return the default 0.3 — matches the * declared schema default and what the manifest advertises. * - `null` (operator-explicit JSON null) → return `undefined`, which * disables both the trace request and the threshold filter. * - Invalid types (string, object, NaN, Infinity, negative) → fall back * to the default 0.3 rather than silently disabling. Bad config should * not turn the safety filter off. * - `> 1` → clamp to 1. * - Valid `[0, 1]` → pass through. * * @internal exported for unit testing */ export declare function parseRecallMinRelevance(value: unknown): number | undefined; /** * Parse the raw `recallRouter` config block into a normalized shape with * sensible defaults. Always returns a fully-populated object so the hook * never has to deal with optional chaining at every access point. * * @internal exported for unit testing */ export declare function parseRecallRouterConfig(value: unknown): { enabled: boolean; mode: "heuristic" | "jina-classifier"; jinaApiKey?: string; classifierId?: string; routes: 2 | 4; labels?: string[]; minConfidence: number; observationFollowsNarrow: boolean; }; export default function (api: MoltbotPluginAPI): void; export declare function buildRetainRequest(transcript: string, messageCount: number, effectiveCtx: PluginHookAgentContext | undefined, pluginConfig: PluginConfig, now?: number, options?: { retentionScope?: "turn" | "window" | "manual"; windowTurns?: number; turnIndex?: number; tags?: string[]; /** * Whether the live Hindsight API supports `update_mode: 'append'`. When * true with `retainDocumentScope: 'session'`, the request gets a stable * per-session document id and `updateMode: 'append'` so each retain * concatenates to the existing document. When false, falls back to a * unique per-turn document id so prior turns aren't overwritten. * Defaults to false (conservative). */ appendSupported?: boolean; }): RetainRequest; export declare function prepareRetentionTranscript(messages: any[], pluginConfig: PluginConfig, retainFullWindow?: boolean): { transcript: string; messageCount: number; } | null; export declare function countUserTurns(messages: any[]): number; export declare function getRetentionTurnIndex(conversationTurnCount: number, retainEveryN: number): number | null; export declare function sliceLastTurnsByUserBoundary(messages: any[], turns: number): any[]; export declare function getClient(): HindsightClient | null;