refactor: new design — sidecar services, moderator Gateway client, tool execute API
- Replace standalone no-reply-api Docker service with unified sidecar (services/main.mjs) that routes /no-reply/* and /moderator/* and starts/stops with openclaw-gateway - Add moderator Discord Gateway client (services/moderator/index.mjs) for real-time MESSAGE_CREATE push instead of polling; notifies plugin via HTTP callback - Add plugin HTTP routes (plugin/web/dirigent-api.ts) for moderator → plugin callbacks (wake-from-dormant, interrupt tail-match) - Fix tool registration format: AgentTool requires execute: not handler:; factory form for tools needing ctx - Rename no-reply-process.ts → sidecar-process.ts, startNoReplyApi → startSideCar - Remove dead config fields from openclaw.plugin.json (humanList, agentList, listMode, channelPoliciesFile, endSymbols, waitIdentifier, multiMessage*, bypassUserIds, etc.) - Rename noReplyPort → sideCarPort - Remove docker-compose.yml, dev-up/down scripts, package-plugin.mjs, test-no-reply-api.mjs - Update install.mjs: clean dist before build, copy services/, drop dead config writes - Update README, Makefile, smoke script for new architecture Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -176,8 +176,6 @@ export function registerAgentEndHook(deps: AgentEndDeps): InterruptFn {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTurnPending(channelId, agentId);
|
||||
|
||||
api.logger.info(
|
||||
`dirigent: agent_end channel=${channelId} agentId=${agentId} empty=${empty} text=${finalText.slice(0, 80)}`,
|
||||
);
|
||||
@@ -186,6 +184,10 @@ export function registerAgentEndHook(deps: AgentEndDeps): InterruptFn {
|
||||
// Real turn: wait for Discord delivery before triggering next speaker.
|
||||
// Anchor was set in before_model_resolve just before the LLM call, so any
|
||||
// message from the agent after the anchor must be from this turn.
|
||||
// NOTE: clearTurnPending is intentionally deferred until after pollForTailMatch
|
||||
// returns. While waiting, isTurnPending remains true so that any re-trigger of
|
||||
// this agent is correctly treated as a self-wakeup (suppressed), preventing it
|
||||
// from starting a second real turn during the tail-match window.
|
||||
const identity = identityRegistry.findByAgentId(agentId);
|
||||
if (identity && moderatorBotToken) {
|
||||
const anchorId = getAnchor(channelId, agentId) ?? "0";
|
||||
@@ -202,6 +204,7 @@ export function registerAgentEndHook(deps: AgentEndDeps): InterruptFn {
|
||||
if (isDormant(channelId)) {
|
||||
// Channel is dormant: a new external message woke it — restart from first speaker
|
||||
api.logger.info(`dirigent: tail-match interrupted (dormant) channel=${channelId} — waking`);
|
||||
clearTurnPending(channelId, agentId);
|
||||
const first = wakeFromDormant(channelId);
|
||||
if (first) await triggerNextSpeaker(channelId, first);
|
||||
return;
|
||||
@@ -243,6 +246,12 @@ export function registerAgentEndHook(deps: AgentEndDeps): InterruptFn {
|
||||
advancingChannels.delete(channelId);
|
||||
}
|
||||
|
||||
// Clear turn pending AFTER advanceSpeaker completes. This ensures isTurnPending
|
||||
// remains true during the async rebuildFn window at cycle boundaries, preventing
|
||||
// re-triggers from starting a second real turn while currentIndex is still at the
|
||||
// outgoing speaker's position.
|
||||
clearTurnPending(channelId, agentId);
|
||||
|
||||
if (enteredDormant) {
|
||||
api.logger.info(`dirigent: channel=${channelId} entered dormant`);
|
||||
if (mode === "discussion") {
|
||||
|
||||
Reference in New Issue
Block a user