You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Hook plugins were defined in the protocol but never actually wired —
RegisterPluginHooks had zero callers and there was no way for a
plugin to push a follow-up message back to the chat it received a
hook for. This change makes the whole loop work and adds a UI for
operators to control which agents each plugin attaches to.
## Protocol — chat.send
New method `chat.send` (plugin → fastclaw notification) that
delivers a fresh OutboundMessage to a specific chat. The plugin
manager turns it into a bus.OutboundMessage with optional
MediaItems (base64-encoded inline) and pushes onto bus.Outbound —
same path the agent's own reply takes, so the chatter sees a
second bubble. Unlike message.inbound (which spawns another agent
turn), chat.send skips the agent entirely.
HookFireParams gains Channel + AccountID so plugins receiving a
hook event have the full bus routing triple needed to echo back to
chat.send. Populated at every hooks.Run callsite in the agent
loop from msg.Channel / msg.AccountID.
## Ordering vs the agent's main reply
PostTurn hooks fire while the agent loop is still finishing, so a
fast plugin could win the bus.Outbound enqueue race and the chatter
would see the plugin's follow-up before the agent's actual reply.
chat.send dispatch is now async with a small default 50ms delay
(tunable via FASTCLAW_PLUGIN_CHAT_SEND_DELAY_MS) — gives the
gateway's microsecond-scale bus push a head start. Channel adapters
serialize sends per account, so bus enqueue order = user-visible
order.
## Wiring + per-agent enable
`registerHookPluginsForAgent` (new) attaches a plugin's hooks to
ONE agent's HookRegistry. Called from loadUserSpace (owner path)
AND EnsureAgent (foreign-attach path) so chatters reaching an
agent via channel binding get the same hooks the owner does.
Default is OPT-IN: a plugin enabled system-wide only means its
process runs and is available to attach. Each agent must individually
set `plugins.enabled[<id>] = true` (via the dashboard Plugins tab
or directly in the configs table) for hooks to fire on its turns.
Rationale: hook plugins can change agent behavior in surprising ways
(extra messages, modified prompts, recorded conversation data).
Default-deny avoids accidentally affecting agents the operator
didn't intend.
UserSpace gains a borrowed PluginMgr pointer so EnsureAgent doesn't
need to reach back to the gateway for it.
## Dashboard
- New `GET /api/plugins/hook` endpoint: read-only metadata listing
of hook-type plugins (id / name / description / version). Not
admin-gated — agent owners need it to populate the Plugins tab.
- handleUpdateAgent: accepts `plugins` map + `pluginsReset` flag;
GET returns `plugins` per-agent overlay. Patch semantics so
flipping one plugin doesn't clobber overrides for siblings.
- New dedicated Plugins tab in agent-settings-dialog (Plug icon),
positioned between Skills and Channels — mirrors the Skills page
layout (header + grid of cards). Replaces the inline card on the
Context page (now removed; Context only carries Multi-bubble +
Auto-remember).
- HookPlugin TypeScript type + listHookPlugins() client.
## Demo
`plugins/post-turn-echo-demo/` — minimal Python plugin (~100 lines)
that registers on PostTurn and ships a fixed follow-up text via
chat.send. Skeleton for richer plugins (audio replies, translations,
CRM sync, etc).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0 commit comments