2026-02-18 01:34:35 +00:00
|
|
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
2026-01-19 13:11:31 +01:00
|
|
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
2026-02-01 10:03:47 +09:00
|
|
|
import type { Command } from "commander";
|
2026-01-16 00:39:22 +00:00
|
|
|
import type { AuthProfileCredential, OAuthCredential } from "../agents/auth-profiles/types.js";
|
2026-01-11 12:11:12 +00:00
|
|
|
import type { AnyAgentTool } from "../agents/tools/common.js";
|
2026-02-01 10:03:47 +09:00
|
|
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
2026-01-15 02:42:41 +00:00
|
|
|
import type { ChannelDock } from "../channels/dock.js";
|
2026-02-08 18:07:13 +01:00
|
|
|
import type { ChannelId, ChannelPlugin } from "../channels/plugins/types.js";
|
2026-02-01 10:03:47 +09:00
|
|
|
import type { createVpsAwareOAuthHandlers } from "../commands/oauth-flow.js";
|
2026-01-30 03:15:10 +01:00
|
|
|
import type { OpenClawConfig } from "../config/config.js";
|
2026-02-01 10:03:47 +09:00
|
|
|
import type { ModelProviderConfig } from "../config/types.js";
|
|
|
|
|
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
|
2026-01-18 05:56:59 +00:00
|
|
|
import type { InternalHookHandler } from "../hooks/internal-hooks.js";
|
|
|
|
|
import type { HookEntry } from "../hooks/types.js";
|
2026-01-16 00:39:22 +00:00
|
|
|
import type { RuntimeEnv } from "../runtime.js";
|
|
|
|
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
2026-01-18 02:14:07 +00:00
|
|
|
import type { PluginRuntime } from "./runtime/types.js";
|
|
|
|
|
|
|
|
|
|
export type { PluginRuntime } from "./runtime/types.js";
|
2026-02-09 10:05:38 -08:00
|
|
|
export type { AnyAgentTool } from "../agents/tools/common.js";
|
2026-01-11 12:11:12 +00:00
|
|
|
|
|
|
|
|
export type PluginLogger = {
|
|
|
|
|
debug?: (message: string) => void;
|
|
|
|
|
info: (message: string) => void;
|
|
|
|
|
warn: (message: string) => void;
|
|
|
|
|
error: (message: string) => void;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-12 01:16:39 +00:00
|
|
|
export type PluginConfigUiHint = {
|
|
|
|
|
label?: string;
|
|
|
|
|
help?: string;
|
2026-02-22 15:17:07 -06:00
|
|
|
tags?: string[];
|
2026-01-12 01:16:39 +00:00
|
|
|
advanced?: boolean;
|
|
|
|
|
sensitive?: boolean;
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-18 02:12:01 +00:00
|
|
|
export type PluginKind = "memory";
|
|
|
|
|
|
2026-01-11 12:11:12 +00:00
|
|
|
export type PluginConfigValidation =
|
|
|
|
|
| { ok: true; value?: unknown }
|
|
|
|
|
| { ok: false; errors: string[] };
|
|
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
export type OpenClawPluginConfigSchema = {
|
2026-01-11 12:11:12 +00:00
|
|
|
safeParse?: (value: unknown) => {
|
|
|
|
|
success: boolean;
|
|
|
|
|
data?: unknown;
|
|
|
|
|
error?: {
|
|
|
|
|
issues?: Array<{ path: Array<string | number>; message: string }>;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
parse?: (value: unknown) => unknown;
|
|
|
|
|
validate?: (value: unknown) => PluginConfigValidation;
|
2026-01-12 01:16:39 +00:00
|
|
|
uiHints?: Record<string, PluginConfigUiHint>;
|
2026-01-16 14:13:30 -06:00
|
|
|
jsonSchema?: Record<string, unknown>;
|
2026-01-11 12:11:12 +00:00
|
|
|
};
|
|
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
export type OpenClawPluginToolContext = {
|
|
|
|
|
config?: OpenClawConfig;
|
2026-01-11 12:11:12 +00:00
|
|
|
workspaceDir?: string;
|
|
|
|
|
agentDir?: string;
|
|
|
|
|
agentId?: string;
|
|
|
|
|
sessionKey?: string;
|
2026-01-13 06:16:43 +00:00
|
|
|
messageChannel?: string;
|
2026-01-11 12:11:12 +00:00
|
|
|
agentAccountId?: string;
|
2026-03-01 20:51:45 -06:00
|
|
|
/** Trusted sender id from inbound context (runtime-provided, not tool args). */
|
|
|
|
|
requesterSenderId?: string;
|
|
|
|
|
/** Whether the trusted sender is an owner. */
|
|
|
|
|
senderIsOwner?: boolean;
|
2026-01-11 12:11:12 +00:00
|
|
|
sandboxed?: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
export type OpenClawPluginToolFactory = (
|
|
|
|
|
ctx: OpenClawPluginToolContext,
|
2026-01-11 12:11:12 +00:00
|
|
|
) => AnyAgentTool | AnyAgentTool[] | null | undefined;
|
|
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
export type OpenClawPluginToolOptions = {
|
2026-01-18 04:07:19 +00:00
|
|
|
name?: string;
|
|
|
|
|
names?: string[];
|
|
|
|
|
optional?: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
export type OpenClawPluginHookOptions = {
|
2026-01-18 05:56:59 +00:00
|
|
|
entry?: HookEntry;
|
|
|
|
|
name?: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
register?: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-16 00:39:22 +00:00
|
|
|
export type ProviderAuthKind = "oauth" | "api_key" | "token" | "device_code" | "custom";
|
|
|
|
|
|
|
|
|
|
export type ProviderAuthResult = {
|
|
|
|
|
profiles: Array<{ profileId: string; credential: AuthProfileCredential }>;
|
2026-01-30 03:15:10 +01:00
|
|
|
configPatch?: Partial<OpenClawConfig>;
|
2026-01-16 00:39:22 +00:00
|
|
|
defaultModel?: string;
|
|
|
|
|
notes?: string[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type ProviderAuthContext = {
|
2026-01-30 03:15:10 +01:00
|
|
|
config: OpenClawConfig;
|
2026-01-16 00:39:22 +00:00
|
|
|
agentDir?: string;
|
|
|
|
|
workspaceDir?: string;
|
|
|
|
|
prompter: WizardPrompter;
|
|
|
|
|
runtime: RuntimeEnv;
|
|
|
|
|
isRemote: boolean;
|
|
|
|
|
openUrl: (url: string) => Promise<void>;
|
|
|
|
|
oauth: {
|
|
|
|
|
createVpsAwareHandlers: typeof createVpsAwareOAuthHandlers;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type ProviderAuthMethod = {
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
hint?: string;
|
|
|
|
|
kind: ProviderAuthKind;
|
|
|
|
|
run: (ctx: ProviderAuthContext) => Promise<ProviderAuthResult>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type ProviderPlugin = {
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
docsPath?: string;
|
|
|
|
|
aliases?: string[];
|
|
|
|
|
envVars?: string[];
|
|
|
|
|
models?: ModelProviderConfig;
|
|
|
|
|
auth: ProviderAuthMethod[];
|
|
|
|
|
formatApiKey?: (cred: AuthProfileCredential) => string;
|
|
|
|
|
refreshOAuth?: (cred: OAuthCredential) => Promise<OAuthCredential>;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
export type OpenClawPluginGatewayMethod = {
|
2026-01-11 12:11:12 +00:00
|
|
|
method: string;
|
|
|
|
|
handler: GatewayRequestHandler;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-23 03:17:10 +00:00
|
|
|
// =============================================================================
|
|
|
|
|
// Plugin Commands
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Context passed to plugin command handlers.
|
|
|
|
|
*/
|
|
|
|
|
export type PluginCommandContext = {
|
|
|
|
|
/** The sender's identifier (e.g., Telegram user ID) */
|
|
|
|
|
senderId?: string;
|
|
|
|
|
/** The channel/surface (e.g., "telegram", "discord") */
|
|
|
|
|
channel: string;
|
2026-02-08 18:07:13 +01:00
|
|
|
/** Provider channel id (e.g., "telegram") */
|
|
|
|
|
channelId?: ChannelId;
|
2026-01-23 03:17:10 +00:00
|
|
|
/** Whether the sender is on the allowlist */
|
|
|
|
|
isAuthorizedSender: boolean;
|
|
|
|
|
/** Raw command arguments after the command name */
|
|
|
|
|
args?: string;
|
|
|
|
|
/** The full normalized command body */
|
|
|
|
|
commandBody: string;
|
2026-01-30 03:15:10 +01:00
|
|
|
/** Current OpenClaw configuration */
|
|
|
|
|
config: OpenClawConfig;
|
2026-02-08 18:07:13 +01:00
|
|
|
/** Raw "From" value (channel-scoped id) */
|
|
|
|
|
from?: string;
|
|
|
|
|
/** Raw "To" value (channel-scoped id) */
|
|
|
|
|
to?: string;
|
|
|
|
|
/** Account id for multi-account channels */
|
|
|
|
|
accountId?: string;
|
|
|
|
|
/** Thread/topic id if available */
|
|
|
|
|
messageThreadId?: number;
|
2026-01-23 03:17:10 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Result returned by a plugin command handler.
|
|
|
|
|
*/
|
2026-01-25 07:22:36 -05:00
|
|
|
export type PluginCommandResult = ReplyPayload;
|
2026-01-23 03:17:10 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handler function for plugin commands.
|
|
|
|
|
*/
|
|
|
|
|
export type PluginCommandHandler = (
|
|
|
|
|
ctx: PluginCommandContext,
|
|
|
|
|
) => PluginCommandResult | Promise<PluginCommandResult>;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Definition for a plugin-registered command.
|
|
|
|
|
*/
|
2026-01-30 03:15:10 +01:00
|
|
|
export type OpenClawPluginCommandDefinition = {
|
2026-01-24 09:40:18 +00:00
|
|
|
/** Command name without leading slash (e.g., "tts") */
|
2026-01-23 03:17:10 +00:00
|
|
|
name: string;
|
|
|
|
|
/** Description shown in /help and command menus */
|
|
|
|
|
description: string;
|
|
|
|
|
/** Whether this command accepts arguments */
|
|
|
|
|
acceptsArgs?: boolean;
|
|
|
|
|
/** Whether only authorized senders can use this command (default: true) */
|
|
|
|
|
requireAuth?: boolean;
|
|
|
|
|
/** The handler function */
|
|
|
|
|
handler: PluginCommandHandler;
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-02 16:22:31 +00:00
|
|
|
export type OpenClawPluginHttpRouteAuth = "gateway" | "plugin";
|
|
|
|
|
export type OpenClawPluginHttpRouteMatch = "exact" | "prefix";
|
2026-01-15 05:03:50 +00:00
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
export type OpenClawPluginHttpRouteHandler = (
|
2026-01-25 07:22:36 -05:00
|
|
|
req: IncomingMessage,
|
|
|
|
|
res: ServerResponse,
|
2026-03-02 16:22:31 +00:00
|
|
|
) => Promise<boolean | void> | boolean | void;
|
|
|
|
|
|
|
|
|
|
export type OpenClawPluginHttpRouteParams = {
|
|
|
|
|
path: string;
|
|
|
|
|
handler: OpenClawPluginHttpRouteHandler;
|
2026-03-02 16:47:51 +00:00
|
|
|
auth: OpenClawPluginHttpRouteAuth;
|
2026-03-02 16:22:31 +00:00
|
|
|
match?: OpenClawPluginHttpRouteMatch;
|
2026-03-02 16:47:51 +00:00
|
|
|
replaceExisting?: boolean;
|
2026-03-02 16:22:31 +00:00
|
|
|
};
|
2026-01-25 07:22:36 -05:00
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
export type OpenClawPluginCliContext = {
|
2026-01-11 12:11:12 +00:00
|
|
|
program: Command;
|
2026-01-30 03:15:10 +01:00
|
|
|
config: OpenClawConfig;
|
2026-01-11 12:11:12 +00:00
|
|
|
workspaceDir?: string;
|
|
|
|
|
logger: PluginLogger;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
export type OpenClawPluginCliRegistrar = (ctx: OpenClawPluginCliContext) => void | Promise<void>;
|
2026-01-11 12:11:12 +00:00
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
export type OpenClawPluginServiceContext = {
|
|
|
|
|
config: OpenClawConfig;
|
2026-01-11 12:11:12 +00:00
|
|
|
workspaceDir?: string;
|
|
|
|
|
stateDir: string;
|
|
|
|
|
logger: PluginLogger;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
export type OpenClawPluginService = {
|
2026-01-11 12:11:12 +00:00
|
|
|
id: string;
|
2026-01-30 03:15:10 +01:00
|
|
|
start: (ctx: OpenClawPluginServiceContext) => void | Promise<void>;
|
|
|
|
|
stop?: (ctx: OpenClawPluginServiceContext) => void | Promise<void>;
|
2026-01-11 12:11:12 +00:00
|
|
|
};
|
|
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
export type OpenClawPluginChannelRegistration = {
|
2026-01-15 02:42:41 +00:00
|
|
|
plugin: ChannelPlugin;
|
|
|
|
|
dock?: ChannelDock;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
export type OpenClawPluginDefinition = {
|
2026-01-11 12:11:12 +00:00
|
|
|
id?: string;
|
|
|
|
|
name?: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
version?: string;
|
2026-01-18 02:12:01 +00:00
|
|
|
kind?: PluginKind;
|
2026-01-30 03:15:10 +01:00
|
|
|
configSchema?: OpenClawPluginConfigSchema;
|
|
|
|
|
register?: (api: OpenClawPluginApi) => void | Promise<void>;
|
|
|
|
|
activate?: (api: OpenClawPluginApi) => void | Promise<void>;
|
2026-01-11 12:11:12 +00:00
|
|
|
};
|
|
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
export type OpenClawPluginModule =
|
|
|
|
|
| OpenClawPluginDefinition
|
|
|
|
|
| ((api: OpenClawPluginApi) => void | Promise<void>);
|
2026-01-11 12:11:12 +00:00
|
|
|
|
2026-01-30 03:15:10 +01:00
|
|
|
export type OpenClawPluginApi = {
|
2026-01-11 12:11:12 +00:00
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
version?: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
source: string;
|
2026-01-30 03:15:10 +01:00
|
|
|
config: OpenClawConfig;
|
2026-01-11 12:11:12 +00:00
|
|
|
pluginConfig?: Record<string, unknown>;
|
2026-01-18 02:14:07 +00:00
|
|
|
runtime: PluginRuntime;
|
2026-01-11 12:11:12 +00:00
|
|
|
logger: PluginLogger;
|
|
|
|
|
registerTool: (
|
2026-01-30 03:15:10 +01:00
|
|
|
tool: AnyAgentTool | OpenClawPluginToolFactory,
|
|
|
|
|
opts?: OpenClawPluginToolOptions,
|
2026-01-11 12:11:12 +00:00
|
|
|
) => void;
|
2026-01-18 05:56:59 +00:00
|
|
|
registerHook: (
|
|
|
|
|
events: string | string[],
|
|
|
|
|
handler: InternalHookHandler,
|
2026-01-30 03:15:10 +01:00
|
|
|
opts?: OpenClawPluginHookOptions,
|
2026-01-18 05:56:59 +00:00
|
|
|
) => void;
|
2026-03-02 16:22:31 +00:00
|
|
|
registerHttpRoute: (params: OpenClawPluginHttpRouteParams) => void;
|
2026-01-30 03:15:10 +01:00
|
|
|
registerChannel: (registration: OpenClawPluginChannelRegistration | ChannelPlugin) => void;
|
2026-01-14 14:31:43 +00:00
|
|
|
registerGatewayMethod: (method: string, handler: GatewayRequestHandler) => void;
|
2026-01-30 03:15:10 +01:00
|
|
|
registerCli: (registrar: OpenClawPluginCliRegistrar, opts?: { commands?: string[] }) => void;
|
|
|
|
|
registerService: (service: OpenClawPluginService) => void;
|
2026-01-16 00:39:22 +00:00
|
|
|
registerProvider: (provider: ProviderPlugin) => void;
|
2026-01-23 03:17:10 +00:00
|
|
|
/**
|
|
|
|
|
* Register a custom command that bypasses the LLM agent.
|
|
|
|
|
* Plugin commands are processed before built-in commands and before agent invocation.
|
|
|
|
|
* Use this for simple state-toggling or status commands that don't need AI reasoning.
|
|
|
|
|
*/
|
2026-01-30 03:15:10 +01:00
|
|
|
registerCommand: (command: OpenClawPluginCommandDefinition) => void;
|
2026-01-11 12:11:12 +00:00
|
|
|
resolvePath: (input: string) => string;
|
2026-01-18 05:40:58 +00:00
|
|
|
/** Register a lifecycle hook handler */
|
|
|
|
|
on: <K extends PluginHookName>(
|
|
|
|
|
hookName: K,
|
|
|
|
|
handler: PluginHookHandlerMap[K],
|
|
|
|
|
opts?: { priority?: number },
|
|
|
|
|
) => void;
|
2026-01-11 12:11:12 +00:00
|
|
|
};
|
|
|
|
|
|
2026-01-17 09:33:56 +00:00
|
|
|
export type PluginOrigin = "bundled" | "global" | "workspace" | "config";
|
2026-01-11 12:11:12 +00:00
|
|
|
|
|
|
|
|
export type PluginDiagnostic = {
|
|
|
|
|
level: "warn" | "error";
|
|
|
|
|
message: string;
|
|
|
|
|
pluginId?: string;
|
|
|
|
|
source?: string;
|
|
|
|
|
};
|
2026-01-18 05:40:58 +00:00
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Plugin Hooks
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
export type PluginHookName =
|
2026-02-17 03:28:10 +01:00
|
|
|
| "before_model_resolve"
|
|
|
|
|
| "before_prompt_build"
|
2026-01-18 05:40:58 +00:00
|
|
|
| "before_agent_start"
|
2026-02-15 14:01:00 -08:00
|
|
|
| "llm_input"
|
|
|
|
|
| "llm_output"
|
2026-01-18 05:40:58 +00:00
|
|
|
| "agent_end"
|
|
|
|
|
| "before_compaction"
|
|
|
|
|
| "after_compaction"
|
Plugin API: compaction/reset hooks, bootstrap file globs, memory plugin status (#13287)
* feat: add before_compaction and before_reset plugin hooks with session context
- Pass session messages to before_compaction hook
- Add before_reset plugin hook for /new and /reset commands
- Add sessionId to plugin hook agent context
* feat: extraBootstrapFiles config with glob pattern support
Add extraBootstrapFiles to agent defaults config, allowing glob patterns
(e.g. "projects/*/TOOLS.md") to auto-load project-level bootstrap files
into agent context every turn. Missing files silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(status): show custom memory plugins as enabled, not unavailable
The status command probes memory availability using the built-in
memory-core manager. Custom memory plugins (e.g. via plugin slot)
can't be probed this way, so they incorrectly showed "unavailable".
Now they show "enabled (plugin X)" without the misleading label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use async fs.glob and capture pre-compaction messages
- Replace globSync (node:fs) with fs.glob (node:fs/promises) to match
codebase conventions for async file operations
- Capture session.messages BEFORE replaceMessages(limited) so
before_compaction hook receives the full conversation history,
not the already-truncated list
* fix: resolve lint errors from CI (oxlint strict mode)
- Add void to fire-and-forget IIFE (no-floating-promises)
- Use String() for unknown catch params in template literals
- Add curly braces to single-statement if (curly rule)
* fix: resolve remaining CI lint errors in workspace.ts
- Remove `| string` from WorkspaceBootstrapFileName union (made all
typeof members redundant per no-redundant-type-constituents)
- Use type assertion for extra bootstrap file names
- Drop redundant await on fs.glob() AsyncIterable (await-thenable)
* fix: address Greptile review — path traversal guard + fs/promises import
- workspace.ts: use path.resolve() + traversal check in loadExtraBootstrapFiles()
- commands-core.ts: import fs from node:fs/promises, drop fs.promises prefix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve symlinks before workspace boundary check
Greptile correctly identified that symlinks inside the workspace could
point to files outside it, bypassing the path prefix check. Now uses
fs.realpath() to resolve symlinks before verifying the real path stays
within the workspace boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Greptile review — hook reliability and type safety
1. before_compaction: add compactingCount field so plugins know both
the full pre-compaction message count and the truncated count being
fed to the compaction LLM. Clarify semantics in comment.
2. loadExtraBootstrapFiles: use path.basename() for the name field
so "projects/quaid/TOOLS.md" maps to the known "TOOLS.md" type
instead of an invalid WorkspaceBootstrapFileName cast.
3. before_reset: fire the hook even when no session file exists.
Previously, short sessions without a persisted file would silently
skip the hook. Now fires with empty messages array so plugins
always know a reset occurred.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate bootstrap filenames and add compaction hook timeout
- Only load extra bootstrap files whose basename matches a recognized
workspace filename (AGENTS.md, TOOLS.md, etc.), preventing arbitrary
files from being injected into agent context.
- Wrap before_compaction hook in a 30-second Promise.race timeout so
misbehaving plugins cannot stall the compaction pipeline.
- Clarify hook comments: before_compaction is intentionally awaited
(plugins need messages before they're discarded) but bounded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make before_compaction non-blocking, add sessionFile to after_compaction
- before_compaction is now true fire-and-forget — no await, no timeout.
Plugins that need full conversation data should persist it themselves
and return quickly, or use after_compaction for async processing.
- after_compaction now includes sessionFile path so plugins can read
the full JSONL transcript asynchronously. All pre-compaction messages
are preserved on disk, eliminating the need to block compaction.
- Removes Promise.race timeout pattern that didn't actually cancel
slow hooks (just raced past them while they continued running).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add sessionFile to before_compaction for parallel processing
The session JSONL already has all messages on disk before compaction
starts. By providing sessionFile in before_compaction, plugins can
read and extract data in parallel with the compaction LLM call rather
than waiting for after_compaction. This is the optimal path for memory
plugins that need the full conversation history.
sessionFile is also kept on after_compaction for plugins that only
need to act after compaction completes (analytics, cleanup, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move bootstrap extras into bundled hook
---------
Co-authored-by: Solomon Steadman <solstead@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clawdbot <clawdbot@alfie.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 06:45:45 +07:00
|
|
|
| "before_reset"
|
2026-01-18 05:40:58 +00:00
|
|
|
| "message_received"
|
|
|
|
|
| "message_sending"
|
|
|
|
|
| "message_sent"
|
|
|
|
|
| "before_tool_call"
|
|
|
|
|
| "after_tool_call"
|
2026-01-19 13:11:31 +01:00
|
|
|
| "tool_result_persist"
|
2026-02-16 00:36:48 -08:00
|
|
|
| "before_message_write"
|
2026-01-18 05:40:58 +00:00
|
|
|
| "session_start"
|
|
|
|
|
| "session_end"
|
2026-02-21 16:14:55 +01:00
|
|
|
| "subagent_spawning"
|
|
|
|
|
| "subagent_delivery_target"
|
|
|
|
|
| "subagent_spawned"
|
|
|
|
|
| "subagent_ended"
|
2026-01-18 05:40:58 +00:00
|
|
|
| "gateway_start"
|
|
|
|
|
| "gateway_stop";
|
|
|
|
|
|
|
|
|
|
// Agent context shared across agent hooks
|
|
|
|
|
export type PluginHookAgentContext = {
|
|
|
|
|
agentId?: string;
|
|
|
|
|
sessionKey?: string;
|
Plugin API: compaction/reset hooks, bootstrap file globs, memory plugin status (#13287)
* feat: add before_compaction and before_reset plugin hooks with session context
- Pass session messages to before_compaction hook
- Add before_reset plugin hook for /new and /reset commands
- Add sessionId to plugin hook agent context
* feat: extraBootstrapFiles config with glob pattern support
Add extraBootstrapFiles to agent defaults config, allowing glob patterns
(e.g. "projects/*/TOOLS.md") to auto-load project-level bootstrap files
into agent context every turn. Missing files silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(status): show custom memory plugins as enabled, not unavailable
The status command probes memory availability using the built-in
memory-core manager. Custom memory plugins (e.g. via plugin slot)
can't be probed this way, so they incorrectly showed "unavailable".
Now they show "enabled (plugin X)" without the misleading label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use async fs.glob and capture pre-compaction messages
- Replace globSync (node:fs) with fs.glob (node:fs/promises) to match
codebase conventions for async file operations
- Capture session.messages BEFORE replaceMessages(limited) so
before_compaction hook receives the full conversation history,
not the already-truncated list
* fix: resolve lint errors from CI (oxlint strict mode)
- Add void to fire-and-forget IIFE (no-floating-promises)
- Use String() for unknown catch params in template literals
- Add curly braces to single-statement if (curly rule)
* fix: resolve remaining CI lint errors in workspace.ts
- Remove `| string` from WorkspaceBootstrapFileName union (made all
typeof members redundant per no-redundant-type-constituents)
- Use type assertion for extra bootstrap file names
- Drop redundant await on fs.glob() AsyncIterable (await-thenable)
* fix: address Greptile review — path traversal guard + fs/promises import
- workspace.ts: use path.resolve() + traversal check in loadExtraBootstrapFiles()
- commands-core.ts: import fs from node:fs/promises, drop fs.promises prefix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve symlinks before workspace boundary check
Greptile correctly identified that symlinks inside the workspace could
point to files outside it, bypassing the path prefix check. Now uses
fs.realpath() to resolve symlinks before verifying the real path stays
within the workspace boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Greptile review — hook reliability and type safety
1. before_compaction: add compactingCount field so plugins know both
the full pre-compaction message count and the truncated count being
fed to the compaction LLM. Clarify semantics in comment.
2. loadExtraBootstrapFiles: use path.basename() for the name field
so "projects/quaid/TOOLS.md" maps to the known "TOOLS.md" type
instead of an invalid WorkspaceBootstrapFileName cast.
3. before_reset: fire the hook even when no session file exists.
Previously, short sessions without a persisted file would silently
skip the hook. Now fires with empty messages array so plugins
always know a reset occurred.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate bootstrap filenames and add compaction hook timeout
- Only load extra bootstrap files whose basename matches a recognized
workspace filename (AGENTS.md, TOOLS.md, etc.), preventing arbitrary
files from being injected into agent context.
- Wrap before_compaction hook in a 30-second Promise.race timeout so
misbehaving plugins cannot stall the compaction pipeline.
- Clarify hook comments: before_compaction is intentionally awaited
(plugins need messages before they're discarded) but bounded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make before_compaction non-blocking, add sessionFile to after_compaction
- before_compaction is now true fire-and-forget — no await, no timeout.
Plugins that need full conversation data should persist it themselves
and return quickly, or use after_compaction for async processing.
- after_compaction now includes sessionFile path so plugins can read
the full JSONL transcript asynchronously. All pre-compaction messages
are preserved on disk, eliminating the need to block compaction.
- Removes Promise.race timeout pattern that didn't actually cancel
slow hooks (just raced past them while they continued running).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add sessionFile to before_compaction for parallel processing
The session JSONL already has all messages on disk before compaction
starts. By providing sessionFile in before_compaction, plugins can
read and extract data in parallel with the compaction LLM call rather
than waiting for after_compaction. This is the optimal path for memory
plugins that need the full conversation history.
sessionFile is also kept on after_compaction for plugins that only
need to act after compaction completes (analytics, cleanup, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move bootstrap extras into bundled hook
---------
Co-authored-by: Solomon Steadman <solstead@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clawdbot <clawdbot@alfie.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 06:45:45 +07:00
|
|
|
sessionId?: string;
|
2026-01-18 05:40:58 +00:00
|
|
|
workspaceDir?: string;
|
|
|
|
|
messageProvider?: string;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-17 03:28:10 +01:00
|
|
|
// before_model_resolve hook
|
|
|
|
|
export type PluginHookBeforeModelResolveEvent = {
|
|
|
|
|
/** User prompt for this run. No session messages are available yet in this phase. */
|
2026-01-18 05:40:58 +00:00
|
|
|
prompt: string;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-17 03:28:10 +01:00
|
|
|
export type PluginHookBeforeModelResolveResult = {
|
2026-02-15 12:05:29 -05:00
|
|
|
/** Override the model for this agent run. E.g. "llama3.3:8b" */
|
|
|
|
|
modelOverride?: string;
|
|
|
|
|
/** Override the provider for this agent run. E.g. "ollama" */
|
|
|
|
|
providerOverride?: string;
|
2026-01-18 05:40:58 +00:00
|
|
|
};
|
|
|
|
|
|
2026-02-17 03:28:10 +01:00
|
|
|
// before_prompt_build hook
|
|
|
|
|
export type PluginHookBeforePromptBuildEvent = {
|
|
|
|
|
prompt: string;
|
|
|
|
|
/** Session messages prepared for this run. */
|
|
|
|
|
messages: unknown[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type PluginHookBeforePromptBuildResult = {
|
|
|
|
|
systemPrompt?: string;
|
|
|
|
|
prependContext?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// before_agent_start hook (legacy compatibility: combines both phases)
|
|
|
|
|
export type PluginHookBeforeAgentStartEvent = {
|
|
|
|
|
prompt: string;
|
|
|
|
|
/** Optional because legacy hook can run in pre-session phase. */
|
|
|
|
|
messages?: unknown[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type PluginHookBeforeAgentStartResult = PluginHookBeforePromptBuildResult &
|
|
|
|
|
PluginHookBeforeModelResolveResult;
|
|
|
|
|
|
2026-02-15 14:01:00 -08:00
|
|
|
// llm_input hook
|
|
|
|
|
export type PluginHookLlmInputEvent = {
|
|
|
|
|
runId: string;
|
|
|
|
|
sessionId: string;
|
|
|
|
|
provider: string;
|
|
|
|
|
model: string;
|
|
|
|
|
systemPrompt?: string;
|
|
|
|
|
prompt: string;
|
|
|
|
|
historyMessages: unknown[];
|
|
|
|
|
imagesCount: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// llm_output hook
|
|
|
|
|
export type PluginHookLlmOutputEvent = {
|
|
|
|
|
runId: string;
|
|
|
|
|
sessionId: string;
|
|
|
|
|
provider: string;
|
|
|
|
|
model: string;
|
|
|
|
|
assistantTexts: string[];
|
|
|
|
|
lastAssistant?: unknown;
|
|
|
|
|
usage?: {
|
|
|
|
|
input?: number;
|
|
|
|
|
output?: number;
|
|
|
|
|
cacheRead?: number;
|
|
|
|
|
cacheWrite?: number;
|
|
|
|
|
total?: number;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-18 05:40:58 +00:00
|
|
|
// agent_end hook
|
|
|
|
|
export type PluginHookAgentEndEvent = {
|
|
|
|
|
messages: unknown[];
|
|
|
|
|
success: boolean;
|
|
|
|
|
error?: string;
|
|
|
|
|
durationMs?: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Compaction hooks
|
|
|
|
|
export type PluginHookBeforeCompactionEvent = {
|
Plugin API: compaction/reset hooks, bootstrap file globs, memory plugin status (#13287)
* feat: add before_compaction and before_reset plugin hooks with session context
- Pass session messages to before_compaction hook
- Add before_reset plugin hook for /new and /reset commands
- Add sessionId to plugin hook agent context
* feat: extraBootstrapFiles config with glob pattern support
Add extraBootstrapFiles to agent defaults config, allowing glob patterns
(e.g. "projects/*/TOOLS.md") to auto-load project-level bootstrap files
into agent context every turn. Missing files silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(status): show custom memory plugins as enabled, not unavailable
The status command probes memory availability using the built-in
memory-core manager. Custom memory plugins (e.g. via plugin slot)
can't be probed this way, so they incorrectly showed "unavailable".
Now they show "enabled (plugin X)" without the misleading label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use async fs.glob and capture pre-compaction messages
- Replace globSync (node:fs) with fs.glob (node:fs/promises) to match
codebase conventions for async file operations
- Capture session.messages BEFORE replaceMessages(limited) so
before_compaction hook receives the full conversation history,
not the already-truncated list
* fix: resolve lint errors from CI (oxlint strict mode)
- Add void to fire-and-forget IIFE (no-floating-promises)
- Use String() for unknown catch params in template literals
- Add curly braces to single-statement if (curly rule)
* fix: resolve remaining CI lint errors in workspace.ts
- Remove `| string` from WorkspaceBootstrapFileName union (made all
typeof members redundant per no-redundant-type-constituents)
- Use type assertion for extra bootstrap file names
- Drop redundant await on fs.glob() AsyncIterable (await-thenable)
* fix: address Greptile review — path traversal guard + fs/promises import
- workspace.ts: use path.resolve() + traversal check in loadExtraBootstrapFiles()
- commands-core.ts: import fs from node:fs/promises, drop fs.promises prefix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve symlinks before workspace boundary check
Greptile correctly identified that symlinks inside the workspace could
point to files outside it, bypassing the path prefix check. Now uses
fs.realpath() to resolve symlinks before verifying the real path stays
within the workspace boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Greptile review — hook reliability and type safety
1. before_compaction: add compactingCount field so plugins know both
the full pre-compaction message count and the truncated count being
fed to the compaction LLM. Clarify semantics in comment.
2. loadExtraBootstrapFiles: use path.basename() for the name field
so "projects/quaid/TOOLS.md" maps to the known "TOOLS.md" type
instead of an invalid WorkspaceBootstrapFileName cast.
3. before_reset: fire the hook even when no session file exists.
Previously, short sessions without a persisted file would silently
skip the hook. Now fires with empty messages array so plugins
always know a reset occurred.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate bootstrap filenames and add compaction hook timeout
- Only load extra bootstrap files whose basename matches a recognized
workspace filename (AGENTS.md, TOOLS.md, etc.), preventing arbitrary
files from being injected into agent context.
- Wrap before_compaction hook in a 30-second Promise.race timeout so
misbehaving plugins cannot stall the compaction pipeline.
- Clarify hook comments: before_compaction is intentionally awaited
(plugins need messages before they're discarded) but bounded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make before_compaction non-blocking, add sessionFile to after_compaction
- before_compaction is now true fire-and-forget — no await, no timeout.
Plugins that need full conversation data should persist it themselves
and return quickly, or use after_compaction for async processing.
- after_compaction now includes sessionFile path so plugins can read
the full JSONL transcript asynchronously. All pre-compaction messages
are preserved on disk, eliminating the need to block compaction.
- Removes Promise.race timeout pattern that didn't actually cancel
slow hooks (just raced past them while they continued running).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add sessionFile to before_compaction for parallel processing
The session JSONL already has all messages on disk before compaction
starts. By providing sessionFile in before_compaction, plugins can
read and extract data in parallel with the compaction LLM call rather
than waiting for after_compaction. This is the optimal path for memory
plugins that need the full conversation history.
sessionFile is also kept on after_compaction for plugins that only
need to act after compaction completes (analytics, cleanup, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move bootstrap extras into bundled hook
---------
Co-authored-by: Solomon Steadman <solstead@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clawdbot <clawdbot@alfie.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 06:45:45 +07:00
|
|
|
/** Total messages in the session before any truncation or compaction */
|
2026-01-18 05:40:58 +00:00
|
|
|
messageCount: number;
|
Plugin API: compaction/reset hooks, bootstrap file globs, memory plugin status (#13287)
* feat: add before_compaction and before_reset plugin hooks with session context
- Pass session messages to before_compaction hook
- Add before_reset plugin hook for /new and /reset commands
- Add sessionId to plugin hook agent context
* feat: extraBootstrapFiles config with glob pattern support
Add extraBootstrapFiles to agent defaults config, allowing glob patterns
(e.g. "projects/*/TOOLS.md") to auto-load project-level bootstrap files
into agent context every turn. Missing files silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(status): show custom memory plugins as enabled, not unavailable
The status command probes memory availability using the built-in
memory-core manager. Custom memory plugins (e.g. via plugin slot)
can't be probed this way, so they incorrectly showed "unavailable".
Now they show "enabled (plugin X)" without the misleading label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use async fs.glob and capture pre-compaction messages
- Replace globSync (node:fs) with fs.glob (node:fs/promises) to match
codebase conventions for async file operations
- Capture session.messages BEFORE replaceMessages(limited) so
before_compaction hook receives the full conversation history,
not the already-truncated list
* fix: resolve lint errors from CI (oxlint strict mode)
- Add void to fire-and-forget IIFE (no-floating-promises)
- Use String() for unknown catch params in template literals
- Add curly braces to single-statement if (curly rule)
* fix: resolve remaining CI lint errors in workspace.ts
- Remove `| string` from WorkspaceBootstrapFileName union (made all
typeof members redundant per no-redundant-type-constituents)
- Use type assertion for extra bootstrap file names
- Drop redundant await on fs.glob() AsyncIterable (await-thenable)
* fix: address Greptile review — path traversal guard + fs/promises import
- workspace.ts: use path.resolve() + traversal check in loadExtraBootstrapFiles()
- commands-core.ts: import fs from node:fs/promises, drop fs.promises prefix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve symlinks before workspace boundary check
Greptile correctly identified that symlinks inside the workspace could
point to files outside it, bypassing the path prefix check. Now uses
fs.realpath() to resolve symlinks before verifying the real path stays
within the workspace boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Greptile review — hook reliability and type safety
1. before_compaction: add compactingCount field so plugins know both
the full pre-compaction message count and the truncated count being
fed to the compaction LLM. Clarify semantics in comment.
2. loadExtraBootstrapFiles: use path.basename() for the name field
so "projects/quaid/TOOLS.md" maps to the known "TOOLS.md" type
instead of an invalid WorkspaceBootstrapFileName cast.
3. before_reset: fire the hook even when no session file exists.
Previously, short sessions without a persisted file would silently
skip the hook. Now fires with empty messages array so plugins
always know a reset occurred.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate bootstrap filenames and add compaction hook timeout
- Only load extra bootstrap files whose basename matches a recognized
workspace filename (AGENTS.md, TOOLS.md, etc.), preventing arbitrary
files from being injected into agent context.
- Wrap before_compaction hook in a 30-second Promise.race timeout so
misbehaving plugins cannot stall the compaction pipeline.
- Clarify hook comments: before_compaction is intentionally awaited
(plugins need messages before they're discarded) but bounded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make before_compaction non-blocking, add sessionFile to after_compaction
- before_compaction is now true fire-and-forget — no await, no timeout.
Plugins that need full conversation data should persist it themselves
and return quickly, or use after_compaction for async processing.
- after_compaction now includes sessionFile path so plugins can read
the full JSONL transcript asynchronously. All pre-compaction messages
are preserved on disk, eliminating the need to block compaction.
- Removes Promise.race timeout pattern that didn't actually cancel
slow hooks (just raced past them while they continued running).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add sessionFile to before_compaction for parallel processing
The session JSONL already has all messages on disk before compaction
starts. By providing sessionFile in before_compaction, plugins can
read and extract data in parallel with the compaction LLM call rather
than waiting for after_compaction. This is the optimal path for memory
plugins that need the full conversation history.
sessionFile is also kept on after_compaction for plugins that only
need to act after compaction completes (analytics, cleanup, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move bootstrap extras into bundled hook
---------
Co-authored-by: Solomon Steadman <solstead@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clawdbot <clawdbot@alfie.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 06:45:45 +07:00
|
|
|
/** Messages being fed to the compaction LLM (after history-limit truncation) */
|
|
|
|
|
compactingCount?: number;
|
2026-01-18 05:40:58 +00:00
|
|
|
tokenCount?: number;
|
Plugin API: compaction/reset hooks, bootstrap file globs, memory plugin status (#13287)
* feat: add before_compaction and before_reset plugin hooks with session context
- Pass session messages to before_compaction hook
- Add before_reset plugin hook for /new and /reset commands
- Add sessionId to plugin hook agent context
* feat: extraBootstrapFiles config with glob pattern support
Add extraBootstrapFiles to agent defaults config, allowing glob patterns
(e.g. "projects/*/TOOLS.md") to auto-load project-level bootstrap files
into agent context every turn. Missing files silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(status): show custom memory plugins as enabled, not unavailable
The status command probes memory availability using the built-in
memory-core manager. Custom memory plugins (e.g. via plugin slot)
can't be probed this way, so they incorrectly showed "unavailable".
Now they show "enabled (plugin X)" without the misleading label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use async fs.glob and capture pre-compaction messages
- Replace globSync (node:fs) with fs.glob (node:fs/promises) to match
codebase conventions for async file operations
- Capture session.messages BEFORE replaceMessages(limited) so
before_compaction hook receives the full conversation history,
not the already-truncated list
* fix: resolve lint errors from CI (oxlint strict mode)
- Add void to fire-and-forget IIFE (no-floating-promises)
- Use String() for unknown catch params in template literals
- Add curly braces to single-statement if (curly rule)
* fix: resolve remaining CI lint errors in workspace.ts
- Remove `| string` from WorkspaceBootstrapFileName union (made all
typeof members redundant per no-redundant-type-constituents)
- Use type assertion for extra bootstrap file names
- Drop redundant await on fs.glob() AsyncIterable (await-thenable)
* fix: address Greptile review — path traversal guard + fs/promises import
- workspace.ts: use path.resolve() + traversal check in loadExtraBootstrapFiles()
- commands-core.ts: import fs from node:fs/promises, drop fs.promises prefix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve symlinks before workspace boundary check
Greptile correctly identified that symlinks inside the workspace could
point to files outside it, bypassing the path prefix check. Now uses
fs.realpath() to resolve symlinks before verifying the real path stays
within the workspace boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Greptile review — hook reliability and type safety
1. before_compaction: add compactingCount field so plugins know both
the full pre-compaction message count and the truncated count being
fed to the compaction LLM. Clarify semantics in comment.
2. loadExtraBootstrapFiles: use path.basename() for the name field
so "projects/quaid/TOOLS.md" maps to the known "TOOLS.md" type
instead of an invalid WorkspaceBootstrapFileName cast.
3. before_reset: fire the hook even when no session file exists.
Previously, short sessions without a persisted file would silently
skip the hook. Now fires with empty messages array so plugins
always know a reset occurred.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate bootstrap filenames and add compaction hook timeout
- Only load extra bootstrap files whose basename matches a recognized
workspace filename (AGENTS.md, TOOLS.md, etc.), preventing arbitrary
files from being injected into agent context.
- Wrap before_compaction hook in a 30-second Promise.race timeout so
misbehaving plugins cannot stall the compaction pipeline.
- Clarify hook comments: before_compaction is intentionally awaited
(plugins need messages before they're discarded) but bounded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make before_compaction non-blocking, add sessionFile to after_compaction
- before_compaction is now true fire-and-forget — no await, no timeout.
Plugins that need full conversation data should persist it themselves
and return quickly, or use after_compaction for async processing.
- after_compaction now includes sessionFile path so plugins can read
the full JSONL transcript asynchronously. All pre-compaction messages
are preserved on disk, eliminating the need to block compaction.
- Removes Promise.race timeout pattern that didn't actually cancel
slow hooks (just raced past them while they continued running).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add sessionFile to before_compaction for parallel processing
The session JSONL already has all messages on disk before compaction
starts. By providing sessionFile in before_compaction, plugins can
read and extract data in parallel with the compaction LLM call rather
than waiting for after_compaction. This is the optimal path for memory
plugins that need the full conversation history.
sessionFile is also kept on after_compaction for plugins that only
need to act after compaction completes (analytics, cleanup, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move bootstrap extras into bundled hook
---------
Co-authored-by: Solomon Steadman <solstead@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clawdbot <clawdbot@alfie.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 06:45:45 +07:00
|
|
|
messages?: unknown[];
|
|
|
|
|
/** Path to the session JSONL transcript. All messages are already on disk
|
|
|
|
|
* before compaction starts, so plugins can read this file asynchronously
|
|
|
|
|
* and process in parallel with the compaction LLM call. */
|
|
|
|
|
sessionFile?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// before_reset hook — fired when /new or /reset clears a session
|
|
|
|
|
export type PluginHookBeforeResetEvent = {
|
|
|
|
|
sessionFile?: string;
|
|
|
|
|
messages?: unknown[];
|
|
|
|
|
reason?: string;
|
2026-01-18 05:40:58 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type PluginHookAfterCompactionEvent = {
|
|
|
|
|
messageCount: number;
|
|
|
|
|
tokenCount?: number;
|
|
|
|
|
compactedCount: number;
|
Plugin API: compaction/reset hooks, bootstrap file globs, memory plugin status (#13287)
* feat: add before_compaction and before_reset plugin hooks with session context
- Pass session messages to before_compaction hook
- Add before_reset plugin hook for /new and /reset commands
- Add sessionId to plugin hook agent context
* feat: extraBootstrapFiles config with glob pattern support
Add extraBootstrapFiles to agent defaults config, allowing glob patterns
(e.g. "projects/*/TOOLS.md") to auto-load project-level bootstrap files
into agent context every turn. Missing files silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(status): show custom memory plugins as enabled, not unavailable
The status command probes memory availability using the built-in
memory-core manager. Custom memory plugins (e.g. via plugin slot)
can't be probed this way, so they incorrectly showed "unavailable".
Now they show "enabled (plugin X)" without the misleading label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use async fs.glob and capture pre-compaction messages
- Replace globSync (node:fs) with fs.glob (node:fs/promises) to match
codebase conventions for async file operations
- Capture session.messages BEFORE replaceMessages(limited) so
before_compaction hook receives the full conversation history,
not the already-truncated list
* fix: resolve lint errors from CI (oxlint strict mode)
- Add void to fire-and-forget IIFE (no-floating-promises)
- Use String() for unknown catch params in template literals
- Add curly braces to single-statement if (curly rule)
* fix: resolve remaining CI lint errors in workspace.ts
- Remove `| string` from WorkspaceBootstrapFileName union (made all
typeof members redundant per no-redundant-type-constituents)
- Use type assertion for extra bootstrap file names
- Drop redundant await on fs.glob() AsyncIterable (await-thenable)
* fix: address Greptile review — path traversal guard + fs/promises import
- workspace.ts: use path.resolve() + traversal check in loadExtraBootstrapFiles()
- commands-core.ts: import fs from node:fs/promises, drop fs.promises prefix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve symlinks before workspace boundary check
Greptile correctly identified that symlinks inside the workspace could
point to files outside it, bypassing the path prefix check. Now uses
fs.realpath() to resolve symlinks before verifying the real path stays
within the workspace boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Greptile review — hook reliability and type safety
1. before_compaction: add compactingCount field so plugins know both
the full pre-compaction message count and the truncated count being
fed to the compaction LLM. Clarify semantics in comment.
2. loadExtraBootstrapFiles: use path.basename() for the name field
so "projects/quaid/TOOLS.md" maps to the known "TOOLS.md" type
instead of an invalid WorkspaceBootstrapFileName cast.
3. before_reset: fire the hook even when no session file exists.
Previously, short sessions without a persisted file would silently
skip the hook. Now fires with empty messages array so plugins
always know a reset occurred.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate bootstrap filenames and add compaction hook timeout
- Only load extra bootstrap files whose basename matches a recognized
workspace filename (AGENTS.md, TOOLS.md, etc.), preventing arbitrary
files from being injected into agent context.
- Wrap before_compaction hook in a 30-second Promise.race timeout so
misbehaving plugins cannot stall the compaction pipeline.
- Clarify hook comments: before_compaction is intentionally awaited
(plugins need messages before they're discarded) but bounded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make before_compaction non-blocking, add sessionFile to after_compaction
- before_compaction is now true fire-and-forget — no await, no timeout.
Plugins that need full conversation data should persist it themselves
and return quickly, or use after_compaction for async processing.
- after_compaction now includes sessionFile path so plugins can read
the full JSONL transcript asynchronously. All pre-compaction messages
are preserved on disk, eliminating the need to block compaction.
- Removes Promise.race timeout pattern that didn't actually cancel
slow hooks (just raced past them while they continued running).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add sessionFile to before_compaction for parallel processing
The session JSONL already has all messages on disk before compaction
starts. By providing sessionFile in before_compaction, plugins can
read and extract data in parallel with the compaction LLM call rather
than waiting for after_compaction. This is the optimal path for memory
plugins that need the full conversation history.
sessionFile is also kept on after_compaction for plugins that only
need to act after compaction completes (analytics, cleanup, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move bootstrap extras into bundled hook
---------
Co-authored-by: Solomon Steadman <solstead@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clawdbot <clawdbot@alfie.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 06:45:45 +07:00
|
|
|
/** Path to the session JSONL transcript. All pre-compaction messages are
|
|
|
|
|
* preserved on disk, so plugins can read and process them asynchronously
|
|
|
|
|
* without blocking the compaction pipeline. */
|
|
|
|
|
sessionFile?: string;
|
2026-01-18 05:40:58 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Message context
|
|
|
|
|
export type PluginHookMessageContext = {
|
|
|
|
|
channelId: string;
|
|
|
|
|
accountId?: string;
|
|
|
|
|
conversationId?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// message_received hook
|
|
|
|
|
export type PluginHookMessageReceivedEvent = {
|
|
|
|
|
from: string;
|
|
|
|
|
content: string;
|
|
|
|
|
timestamp?: number;
|
|
|
|
|
metadata?: Record<string, unknown>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// message_sending hook
|
|
|
|
|
export type PluginHookMessageSendingEvent = {
|
|
|
|
|
to: string;
|
|
|
|
|
content: string;
|
|
|
|
|
metadata?: Record<string, unknown>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type PluginHookMessageSendingResult = {
|
|
|
|
|
content?: string;
|
|
|
|
|
cancel?: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// message_sent hook
|
|
|
|
|
export type PluginHookMessageSentEvent = {
|
|
|
|
|
to: string;
|
|
|
|
|
content: string;
|
|
|
|
|
success: boolean;
|
|
|
|
|
error?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Tool context
|
|
|
|
|
export type PluginHookToolContext = {
|
|
|
|
|
agentId?: string;
|
|
|
|
|
sessionKey?: string;
|
|
|
|
|
toolName: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// before_tool_call hook
|
|
|
|
|
export type PluginHookBeforeToolCallEvent = {
|
|
|
|
|
toolName: string;
|
|
|
|
|
params: Record<string, unknown>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type PluginHookBeforeToolCallResult = {
|
|
|
|
|
params?: Record<string, unknown>;
|
|
|
|
|
block?: boolean;
|
|
|
|
|
blockReason?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// after_tool_call hook
|
|
|
|
|
export type PluginHookAfterToolCallEvent = {
|
|
|
|
|
toolName: string;
|
|
|
|
|
params: Record<string, unknown>;
|
|
|
|
|
result?: unknown;
|
|
|
|
|
error?: string;
|
|
|
|
|
durationMs?: number;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-19 13:11:31 +01:00
|
|
|
// tool_result_persist hook
|
|
|
|
|
export type PluginHookToolResultPersistContext = {
|
|
|
|
|
agentId?: string;
|
|
|
|
|
sessionKey?: string;
|
|
|
|
|
toolName?: string;
|
|
|
|
|
toolCallId?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type PluginHookToolResultPersistEvent = {
|
|
|
|
|
toolName?: string;
|
|
|
|
|
toolCallId?: string;
|
|
|
|
|
/**
|
|
|
|
|
* The toolResult message about to be written to the session transcript.
|
|
|
|
|
* Handlers may return a modified message (e.g. drop non-essential fields).
|
|
|
|
|
*/
|
|
|
|
|
message: AgentMessage;
|
|
|
|
|
/** True when the tool result was synthesized by a guard/repair step. */
|
|
|
|
|
isSynthetic?: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type PluginHookToolResultPersistResult = {
|
|
|
|
|
message?: AgentMessage;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-16 00:36:48 -08:00
|
|
|
// before_message_write hook
|
|
|
|
|
export type PluginHookBeforeMessageWriteEvent = {
|
|
|
|
|
message: AgentMessage;
|
|
|
|
|
sessionKey?: string;
|
|
|
|
|
agentId?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type PluginHookBeforeMessageWriteResult = {
|
2026-02-16 23:26:02 +00:00
|
|
|
block?: boolean; // If true, message is NOT written to JSONL
|
2026-02-16 00:36:48 -08:00
|
|
|
message?: AgentMessage; // Optional: modified message to write instead
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-18 05:40:58 +00:00
|
|
|
// Session context
|
|
|
|
|
export type PluginHookSessionContext = {
|
|
|
|
|
agentId?: string;
|
|
|
|
|
sessionId: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// session_start hook
|
|
|
|
|
export type PluginHookSessionStartEvent = {
|
|
|
|
|
sessionId: string;
|
|
|
|
|
resumedFrom?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// session_end hook
|
|
|
|
|
export type PluginHookSessionEndEvent = {
|
|
|
|
|
sessionId: string;
|
|
|
|
|
messageCount: number;
|
|
|
|
|
durationMs?: number;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-21 16:14:55 +01:00
|
|
|
// Subagent context
|
|
|
|
|
export type PluginHookSubagentContext = {
|
|
|
|
|
runId?: string;
|
|
|
|
|
childSessionKey?: string;
|
|
|
|
|
requesterSessionKey?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type PluginHookSubagentTargetKind = "subagent" | "acp";
|
|
|
|
|
|
|
|
|
|
// subagent_spawning hook
|
|
|
|
|
export type PluginHookSubagentSpawningEvent = {
|
|
|
|
|
childSessionKey: string;
|
|
|
|
|
agentId: string;
|
|
|
|
|
label?: string;
|
|
|
|
|
mode: "run" | "session";
|
|
|
|
|
requester?: {
|
|
|
|
|
channel?: string;
|
|
|
|
|
accountId?: string;
|
|
|
|
|
to?: string;
|
|
|
|
|
threadId?: string | number;
|
|
|
|
|
};
|
|
|
|
|
threadRequested: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type PluginHookSubagentSpawningResult =
|
|
|
|
|
| {
|
|
|
|
|
status: "ok";
|
|
|
|
|
threadBindingReady?: boolean;
|
|
|
|
|
}
|
|
|
|
|
| {
|
|
|
|
|
status: "error";
|
|
|
|
|
error: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// subagent_delivery_target hook
|
|
|
|
|
export type PluginHookSubagentDeliveryTargetEvent = {
|
|
|
|
|
childSessionKey: string;
|
|
|
|
|
requesterSessionKey: string;
|
|
|
|
|
requesterOrigin?: {
|
|
|
|
|
channel?: string;
|
|
|
|
|
accountId?: string;
|
|
|
|
|
to?: string;
|
|
|
|
|
threadId?: string | number;
|
|
|
|
|
};
|
|
|
|
|
childRunId?: string;
|
|
|
|
|
spawnMode?: "run" | "session";
|
|
|
|
|
expectsCompletionMessage: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type PluginHookSubagentDeliveryTargetResult = {
|
|
|
|
|
origin?: {
|
|
|
|
|
channel?: string;
|
|
|
|
|
accountId?: string;
|
|
|
|
|
to?: string;
|
|
|
|
|
threadId?: string | number;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// subagent_spawned hook
|
|
|
|
|
export type PluginHookSubagentSpawnedEvent = {
|
|
|
|
|
runId: string;
|
|
|
|
|
childSessionKey: string;
|
|
|
|
|
agentId: string;
|
|
|
|
|
label?: string;
|
|
|
|
|
mode: "run" | "session";
|
|
|
|
|
requester?: {
|
|
|
|
|
channel?: string;
|
|
|
|
|
accountId?: string;
|
|
|
|
|
to?: string;
|
|
|
|
|
threadId?: string | number;
|
|
|
|
|
};
|
|
|
|
|
threadRequested: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// subagent_ended hook
|
|
|
|
|
export type PluginHookSubagentEndedEvent = {
|
|
|
|
|
targetSessionKey: string;
|
|
|
|
|
targetKind: PluginHookSubagentTargetKind;
|
|
|
|
|
reason: string;
|
|
|
|
|
sendFarewell?: boolean;
|
|
|
|
|
accountId?: string;
|
|
|
|
|
runId?: string;
|
|
|
|
|
endedAt?: number;
|
|
|
|
|
outcome?: "ok" | "error" | "timeout" | "killed" | "reset" | "deleted";
|
|
|
|
|
error?: string;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-18 05:40:58 +00:00
|
|
|
// Gateway context
|
|
|
|
|
export type PluginHookGatewayContext = {
|
|
|
|
|
port?: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// gateway_start hook
|
|
|
|
|
export type PluginHookGatewayStartEvent = {
|
|
|
|
|
port: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// gateway_stop hook
|
|
|
|
|
export type PluginHookGatewayStopEvent = {
|
|
|
|
|
reason?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Hook handler types mapped by hook name
|
|
|
|
|
export type PluginHookHandlerMap = {
|
2026-02-17 03:28:10 +01:00
|
|
|
before_model_resolve: (
|
|
|
|
|
event: PluginHookBeforeModelResolveEvent,
|
|
|
|
|
ctx: PluginHookAgentContext,
|
|
|
|
|
) =>
|
|
|
|
|
| Promise<PluginHookBeforeModelResolveResult | void>
|
|
|
|
|
| PluginHookBeforeModelResolveResult
|
|
|
|
|
| void;
|
|
|
|
|
before_prompt_build: (
|
|
|
|
|
event: PluginHookBeforePromptBuildEvent,
|
|
|
|
|
ctx: PluginHookAgentContext,
|
|
|
|
|
) => Promise<PluginHookBeforePromptBuildResult | void> | PluginHookBeforePromptBuildResult | void;
|
2026-01-18 05:40:58 +00:00
|
|
|
before_agent_start: (
|
|
|
|
|
event: PluginHookBeforeAgentStartEvent,
|
|
|
|
|
ctx: PluginHookAgentContext,
|
|
|
|
|
) => Promise<PluginHookBeforeAgentStartResult | void> | PluginHookBeforeAgentStartResult | void;
|
2026-02-15 14:01:00 -08:00
|
|
|
llm_input: (event: PluginHookLlmInputEvent, ctx: PluginHookAgentContext) => Promise<void> | void;
|
|
|
|
|
llm_output: (
|
|
|
|
|
event: PluginHookLlmOutputEvent,
|
|
|
|
|
ctx: PluginHookAgentContext,
|
|
|
|
|
) => Promise<void> | void;
|
2026-01-18 05:40:58 +00:00
|
|
|
agent_end: (event: PluginHookAgentEndEvent, ctx: PluginHookAgentContext) => Promise<void> | void;
|
|
|
|
|
before_compaction: (
|
|
|
|
|
event: PluginHookBeforeCompactionEvent,
|
|
|
|
|
ctx: PluginHookAgentContext,
|
|
|
|
|
) => Promise<void> | void;
|
|
|
|
|
after_compaction: (
|
|
|
|
|
event: PluginHookAfterCompactionEvent,
|
|
|
|
|
ctx: PluginHookAgentContext,
|
|
|
|
|
) => Promise<void> | void;
|
Plugin API: compaction/reset hooks, bootstrap file globs, memory plugin status (#13287)
* feat: add before_compaction and before_reset plugin hooks with session context
- Pass session messages to before_compaction hook
- Add before_reset plugin hook for /new and /reset commands
- Add sessionId to plugin hook agent context
* feat: extraBootstrapFiles config with glob pattern support
Add extraBootstrapFiles to agent defaults config, allowing glob patterns
(e.g. "projects/*/TOOLS.md") to auto-load project-level bootstrap files
into agent context every turn. Missing files silently skipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(status): show custom memory plugins as enabled, not unavailable
The status command probes memory availability using the built-in
memory-core manager. Custom memory plugins (e.g. via plugin slot)
can't be probed this way, so they incorrectly showed "unavailable".
Now they show "enabled (plugin X)" without the misleading label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use async fs.glob and capture pre-compaction messages
- Replace globSync (node:fs) with fs.glob (node:fs/promises) to match
codebase conventions for async file operations
- Capture session.messages BEFORE replaceMessages(limited) so
before_compaction hook receives the full conversation history,
not the already-truncated list
* fix: resolve lint errors from CI (oxlint strict mode)
- Add void to fire-and-forget IIFE (no-floating-promises)
- Use String() for unknown catch params in template literals
- Add curly braces to single-statement if (curly rule)
* fix: resolve remaining CI lint errors in workspace.ts
- Remove `| string` from WorkspaceBootstrapFileName union (made all
typeof members redundant per no-redundant-type-constituents)
- Use type assertion for extra bootstrap file names
- Drop redundant await on fs.glob() AsyncIterable (await-thenable)
* fix: address Greptile review — path traversal guard + fs/promises import
- workspace.ts: use path.resolve() + traversal check in loadExtraBootstrapFiles()
- commands-core.ts: import fs from node:fs/promises, drop fs.promises prefix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve symlinks before workspace boundary check
Greptile correctly identified that symlinks inside the workspace could
point to files outside it, bypassing the path prefix check. Now uses
fs.realpath() to resolve symlinks before verifying the real path stays
within the workspace boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Greptile review — hook reliability and type safety
1. before_compaction: add compactingCount field so plugins know both
the full pre-compaction message count and the truncated count being
fed to the compaction LLM. Clarify semantics in comment.
2. loadExtraBootstrapFiles: use path.basename() for the name field
so "projects/quaid/TOOLS.md" maps to the known "TOOLS.md" type
instead of an invalid WorkspaceBootstrapFileName cast.
3. before_reset: fire the hook even when no session file exists.
Previously, short sessions without a persisted file would silently
skip the hook. Now fires with empty messages array so plugins
always know a reset occurred.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: validate bootstrap filenames and add compaction hook timeout
- Only load extra bootstrap files whose basename matches a recognized
workspace filename (AGENTS.md, TOOLS.md, etc.), preventing arbitrary
files from being injected into agent context.
- Wrap before_compaction hook in a 30-second Promise.race timeout so
misbehaving plugins cannot stall the compaction pipeline.
- Clarify hook comments: before_compaction is intentionally awaited
(plugins need messages before they're discarded) but bounded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make before_compaction non-blocking, add sessionFile to after_compaction
- before_compaction is now true fire-and-forget — no await, no timeout.
Plugins that need full conversation data should persist it themselves
and return quickly, or use after_compaction for async processing.
- after_compaction now includes sessionFile path so plugins can read
the full JSONL transcript asynchronously. All pre-compaction messages
are preserved on disk, eliminating the need to block compaction.
- Removes Promise.race timeout pattern that didn't actually cancel
slow hooks (just raced past them while they continued running).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add sessionFile to before_compaction for parallel processing
The session JSONL already has all messages on disk before compaction
starts. By providing sessionFile in before_compaction, plugins can
read and extract data in parallel with the compaction LLM call rather
than waiting for after_compaction. This is the optimal path for memory
plugins that need the full conversation history.
sessionFile is also kept on after_compaction for plugins that only
need to act after compaction completes (analytics, cleanup, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move bootstrap extras into bundled hook
---------
Co-authored-by: Solomon Steadman <solstead@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clawdbot <clawdbot@alfie.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 06:45:45 +07:00
|
|
|
before_reset: (
|
|
|
|
|
event: PluginHookBeforeResetEvent,
|
|
|
|
|
ctx: PluginHookAgentContext,
|
|
|
|
|
) => Promise<void> | void;
|
2026-01-18 05:40:58 +00:00
|
|
|
message_received: (
|
|
|
|
|
event: PluginHookMessageReceivedEvent,
|
|
|
|
|
ctx: PluginHookMessageContext,
|
|
|
|
|
) => Promise<void> | void;
|
|
|
|
|
message_sending: (
|
|
|
|
|
event: PluginHookMessageSendingEvent,
|
|
|
|
|
ctx: PluginHookMessageContext,
|
|
|
|
|
) => Promise<PluginHookMessageSendingResult | void> | PluginHookMessageSendingResult | void;
|
|
|
|
|
message_sent: (
|
|
|
|
|
event: PluginHookMessageSentEvent,
|
|
|
|
|
ctx: PluginHookMessageContext,
|
|
|
|
|
) => Promise<void> | void;
|
|
|
|
|
before_tool_call: (
|
|
|
|
|
event: PluginHookBeforeToolCallEvent,
|
|
|
|
|
ctx: PluginHookToolContext,
|
|
|
|
|
) => Promise<PluginHookBeforeToolCallResult | void> | PluginHookBeforeToolCallResult | void;
|
|
|
|
|
after_tool_call: (
|
|
|
|
|
event: PluginHookAfterToolCallEvent,
|
|
|
|
|
ctx: PluginHookToolContext,
|
|
|
|
|
) => Promise<void> | void;
|
2026-01-19 13:11:31 +01:00
|
|
|
tool_result_persist: (
|
|
|
|
|
event: PluginHookToolResultPersistEvent,
|
|
|
|
|
ctx: PluginHookToolResultPersistContext,
|
|
|
|
|
) => PluginHookToolResultPersistResult | void;
|
2026-02-16 00:36:48 -08:00
|
|
|
before_message_write: (
|
|
|
|
|
event: PluginHookBeforeMessageWriteEvent,
|
|
|
|
|
ctx: { agentId?: string; sessionKey?: string },
|
|
|
|
|
) => PluginHookBeforeMessageWriteResult | void;
|
2026-01-18 05:40:58 +00:00
|
|
|
session_start: (
|
|
|
|
|
event: PluginHookSessionStartEvent,
|
|
|
|
|
ctx: PluginHookSessionContext,
|
|
|
|
|
) => Promise<void> | void;
|
|
|
|
|
session_end: (
|
|
|
|
|
event: PluginHookSessionEndEvent,
|
|
|
|
|
ctx: PluginHookSessionContext,
|
|
|
|
|
) => Promise<void> | void;
|
2026-02-21 16:14:55 +01:00
|
|
|
subagent_spawning: (
|
|
|
|
|
event: PluginHookSubagentSpawningEvent,
|
|
|
|
|
ctx: PluginHookSubagentContext,
|
|
|
|
|
) => Promise<PluginHookSubagentSpawningResult | void> | PluginHookSubagentSpawningResult | void;
|
|
|
|
|
subagent_delivery_target: (
|
|
|
|
|
event: PluginHookSubagentDeliveryTargetEvent,
|
|
|
|
|
ctx: PluginHookSubagentContext,
|
|
|
|
|
) =>
|
|
|
|
|
| Promise<PluginHookSubagentDeliveryTargetResult | void>
|
|
|
|
|
| PluginHookSubagentDeliveryTargetResult
|
|
|
|
|
| void;
|
|
|
|
|
subagent_spawned: (
|
|
|
|
|
event: PluginHookSubagentSpawnedEvent,
|
|
|
|
|
ctx: PluginHookSubagentContext,
|
|
|
|
|
) => Promise<void> | void;
|
|
|
|
|
subagent_ended: (
|
|
|
|
|
event: PluginHookSubagentEndedEvent,
|
|
|
|
|
ctx: PluginHookSubagentContext,
|
|
|
|
|
) => Promise<void> | void;
|
2026-01-18 05:40:58 +00:00
|
|
|
gateway_start: (
|
|
|
|
|
event: PluginHookGatewayStartEvent,
|
|
|
|
|
ctx: PluginHookGatewayContext,
|
|
|
|
|
) => Promise<void> | void;
|
|
|
|
|
gateway_stop: (
|
|
|
|
|
event: PluginHookGatewayStopEvent,
|
|
|
|
|
ctx: PluginHookGatewayContext,
|
|
|
|
|
) => Promise<void> | void;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type PluginHookRegistration<K extends PluginHookName = PluginHookName> = {
|
|
|
|
|
pluginId: string;
|
|
|
|
|
hookName: K;
|
|
|
|
|
handler: PluginHookHandlerMap[K];
|
|
|
|
|
priority?: number;
|
|
|
|
|
source: string;
|
|
|
|
|
};
|