feat: wait for human reply (waitIdentifier 👤)
- Add waitIdentifier config (default: 👤) to DirigentConfig and plugin schema - Prompt injection: tells agents to end with 👤 when they need a human reply, warns to use sparingly (only when human is actively participating) - Detection in before_message_write and message_sent hooks - Turn manager: new waitingForHuman state - checkTurn() blocks all agents when waiting - onNewMessage() clears state on human message - Non-human messages ignored while waiting - resetTurn() also clears waiting state - All agents routed to no-reply model during waiting state - Update docs (FEAT.md, CHANGELOG.md, TASKLIST.md, README.md)
This commit is contained in:
@@ -11,6 +11,10 @@
|
|||||||
- Agents cycle in their original turn-order position
|
- Agents cycle in their original turn-order position
|
||||||
- After all mentioned agents have spoken, original order restores and state goes dormant
|
- After all mentioned agents have spoken, original order restores and state goes dormant
|
||||||
- Handles edge cases: all NO_REPLY, reset, non-agent mentions
|
- Handles edge cases: all NO_REPLY, reset, non-agent mentions
|
||||||
|
- **Wait for human reply**: New `waitIdentifier` (default: `👤`):
|
||||||
|
- Agent ends with `👤` to signal it needs a human response
|
||||||
|
- All agents go silent (routed to no-reply model) until a human speaks
|
||||||
|
- Prompt injection warns agents to use sparingly (only when human is actively participating)
|
||||||
- **Installer improvements**:
|
- **Installer improvements**:
|
||||||
- Dynamic OpenClaw dir resolution (`$OPENCLAW_DIR` → `openclaw` CLI → `~/.openclaw`)
|
- Dynamic OpenClaw dir resolution (`$OPENCLAW_DIR` → `openclaw` CLI → `~/.openclaw`)
|
||||||
- Plugin installed to `$(openclaw_dir)/plugins/dirigent`
|
- Plugin installed to `$(openclaw_dir)/plugins/dirigent`
|
||||||
|
|||||||
12
FEAT.md
12
FEAT.md
@@ -52,6 +52,18 @@ All implemented features across all versions.
|
|||||||
- Group chat prompts include: agent name, Discord accountId, Discord userId
|
- Group chat prompts include: agent name, Discord accountId, Discord userId
|
||||||
- userId resolved from bot token (base64 first segment)
|
- userId resolved from bot token (base64 first segment)
|
||||||
|
|
||||||
|
## Wait for Human Reply *(v0.3.0)*
|
||||||
|
- Configurable wait identifier (default: `👤`)
|
||||||
|
- Agent ends message with `👤` instead of `🔚` when it needs a human to reply
|
||||||
|
- Triggers "waiting for human" state:
|
||||||
|
- All agents routed to no-reply model
|
||||||
|
- Turn manager goes dormant
|
||||||
|
- State clears automatically when a human sends a message
|
||||||
|
- Prompt injection tells agents:
|
||||||
|
- Use wait identifier only when confident the human is actively participating
|
||||||
|
- Do NOT use it speculatively
|
||||||
|
- Works with mention override: wait identifier during override also triggers waiting state
|
||||||
|
|
||||||
## Scheduling Identifier
|
## Scheduling Identifier
|
||||||
- Configurable identifier (default: `➡️`) used for moderator handoff
|
- Configurable identifier (default: `➡️`) used for moderator handoff
|
||||||
- Handoff format: `<@TARGET_USER_ID>➡️` (non-semantic scheduling signal)
|
- Handoff format: `<@TARGET_USER_ID>➡️` (non-semantic scheduling signal)
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ Common options (see `docs/INTEGRATION.md`):
|
|||||||
- `humanList`, `agentList`
|
- `humanList`, `agentList`
|
||||||
- `endSymbols`
|
- `endSymbols`
|
||||||
- `schedulingIdentifier` (default `➡️`)
|
- `schedulingIdentifier` (default `➡️`)
|
||||||
|
- `waitIdentifier` (default `👤`) — agent ends with this to pause all agents until human replies
|
||||||
- `channelPoliciesFile` (per-channel overrides)
|
- `channelPoliciesFile` (per-channel overrides)
|
||||||
- `moderatorBotToken` (handoff messages)
|
- `moderatorBotToken` (handoff messages)
|
||||||
- `enableDebugLogs`, `debugLogChannelIds`
|
- `enableDebugLogs`, `debugLogChannelIds`
|
||||||
|
|||||||
11
TASKLIST.md
11
TASKLIST.md
@@ -69,6 +69,17 @@
|
|||||||
- New helpers in `index.ts`: `buildUserIdToAccountIdMap()`, `extractMentionedUserIds()`.
|
- New helpers in `index.ts`: `buildUserIdToAccountIdMap()`, `extractMentionedUserIds()`.
|
||||||
- **Done**: Override logic integrated in `message_received` handler.
|
- **Done**: Override logic integrated in `message_received` handler.
|
||||||
|
|
||||||
|
## 8) Wait for Human Reply ✅
|
||||||
|
- Added configurable `waitIdentifier` (default: `👤`) to config and config schema.
|
||||||
|
- Prompt injection in group chats: tells agents to end with `👤` instead of end symbol when they need a human response. Warns to use sparingly — only when human is actively participating.
|
||||||
|
- Detection in `before_message_write` and `message_sent`: if last char matches wait identifier → `setWaitingForHuman(channelId)`.
|
||||||
|
- Turn manager `waitingForHuman` state:
|
||||||
|
- `checkTurn()` blocks all agents (`reason: "waiting_for_human"`) → routed to no-reply model.
|
||||||
|
- `onNewMessage()` clears `waitingForHuman` on human message → normal flow resumes.
|
||||||
|
- Non-human messages ignored while waiting.
|
||||||
|
- `resetTurn()` also clears waiting state.
|
||||||
|
- **Done**: Full lifecycle implemented across turn-manager, rules, and index.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Open Items / Notes
|
## Open Items / Notes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import path from "node:path";
|
|||||||
import { spawn, type ChildProcess } from "node:child_process";
|
import { spawn, type ChildProcess } from "node:child_process";
|
||||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
import { evaluateDecision, resolvePolicy, type ChannelPolicy, type Decision, type DirigentConfig } from "./rules.js";
|
import { evaluateDecision, resolvePolicy, type ChannelPolicy, type Decision, type DirigentConfig } from "./rules.js";
|
||||||
import { checkTurn, advanceTurn, resetTurn, onNewMessage, onSpeakerDone, initTurnOrder, getTurnDebugInfo, setMentionOverride, hasMentionOverride } from "./turn-manager.js";
|
import { checkTurn, advanceTurn, resetTurn, onNewMessage, onSpeakerDone, initTurnOrder, getTurnDebugInfo, setMentionOverride, hasMentionOverride, setWaitingForHuman, isWaitingForHuman } from "./turn-manager.js";
|
||||||
import { startModeratorPresence, stopModeratorPresence } from "./moderator-presence.js";
|
import { startModeratorPresence, stopModeratorPresence } from "./moderator-presence.js";
|
||||||
|
|
||||||
// ── No-Reply API child process lifecycle ──────────────────────────────
|
// ── No-Reply API child process lifecycle ──────────────────────────────
|
||||||
@@ -78,11 +78,12 @@ const sessionTurnHandled = new Set<string>(); // Track sessions where turn was a
|
|||||||
const MAX_SESSION_DECISIONS = 2000;
|
const MAX_SESSION_DECISIONS = 2000;
|
||||||
const DECISION_TTL_MS = 5 * 60 * 1000;
|
const DECISION_TTL_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
function buildEndMarkerInstruction(endSymbols: string[], isGroupChat: boolean, schedulingIdentifier: string): string {
|
function buildEndMarkerInstruction(endSymbols: string[], isGroupChat: boolean, schedulingIdentifier: string, waitIdentifier: string): string {
|
||||||
const symbols = endSymbols.length > 0 ? endSymbols.join("") : "🔚";
|
const symbols = endSymbols.length > 0 ? endSymbols.join("") : "🔚";
|
||||||
let instruction = `Your response MUST end with ${symbols}. Exception: gateway keywords (e.g. NO_REPLY, HEARTBEAT_OK) must NOT include ${symbols}.`;
|
let instruction = `Your response MUST end with ${symbols}. Exception: gateway keywords (e.g. NO_REPLY, HEARTBEAT_OK) must NOT include ${symbols}.`;
|
||||||
if (isGroupChat) {
|
if (isGroupChat) {
|
||||||
instruction += `\n\nGroup chat rules: If this message is not relevant to you, does not need your response, or you have nothing valuable to add, reply with NO_REPLY. Do not speak just for the sake of speaking.`;
|
instruction += `\n\nGroup chat rules: If this message is not relevant to you, does not need your response, or you have nothing valuable to add, reply with NO_REPLY. Do not speak just for the sake of speaking.`;
|
||||||
|
instruction += `\n\nWait for human reply: If you need a human to respond to your message, end with ${waitIdentifier} instead of ${symbols}. This pauses all agents until a human speaks. Use this sparingly — only when you are confident the human is actively participating in the discussion (has sent a message recently). Do NOT use it speculatively.`;
|
||||||
}
|
}
|
||||||
return instruction;
|
return instruction;
|
||||||
}
|
}
|
||||||
@@ -237,6 +238,7 @@ function getLivePluginConfig(api: OpenClawPluginApi, fallback: DirigentConfig):
|
|||||||
enableDebugLogs: false,
|
enableDebugLogs: false,
|
||||||
debugLogChannelIds: [],
|
debugLogChannelIds: [],
|
||||||
schedulingIdentifier: "➡️",
|
schedulingIdentifier: "➡️",
|
||||||
|
waitIdentifier: "👤",
|
||||||
...cfg,
|
...cfg,
|
||||||
} as DirigentConfig;
|
} as DirigentConfig;
|
||||||
}
|
}
|
||||||
@@ -539,6 +541,7 @@ export default {
|
|||||||
enableDirigentPolicyTool: true,
|
enableDirigentPolicyTool: true,
|
||||||
discordControlApiBaseUrl: "http://127.0.0.1:8790",
|
discordControlApiBaseUrl: "http://127.0.0.1:8790",
|
||||||
schedulingIdentifier: "➡️",
|
schedulingIdentifier: "➡️",
|
||||||
|
waitIdentifier: "👤",
|
||||||
...(api.pluginConfig || {}),
|
...(api.pluginConfig || {}),
|
||||||
} as DirigentConfig & {
|
} as DirigentConfig & {
|
||||||
enableDiscordControlTool: boolean;
|
enableDiscordControlTool: boolean;
|
||||||
@@ -1081,7 +1084,8 @@ export default {
|
|||||||
const policy = resolvePolicy(live, derived.channelId, policyState.channelPolicies);
|
const policy = resolvePolicy(live, derived.channelId, policyState.channelPolicies);
|
||||||
const isGroupChat = derived.conv.is_group_chat === true || derived.conv.is_group_chat === "true";
|
const isGroupChat = derived.conv.is_group_chat === true || derived.conv.is_group_chat === "true";
|
||||||
const schedulingId = live.schedulingIdentifier || "➡️";
|
const schedulingId = live.schedulingIdentifier || "➡️";
|
||||||
const instruction = buildEndMarkerInstruction(policy.endSymbols, isGroupChat, schedulingId);
|
const waitId = live.waitIdentifier || "👤";
|
||||||
|
const instruction = buildEndMarkerInstruction(policy.endSymbols, isGroupChat, schedulingId, waitId);
|
||||||
|
|
||||||
// Inject agent identity for group chats (includes userId now)
|
// Inject agent identity for group chats (includes userId now)
|
||||||
let identity = "";
|
let identity = "";
|
||||||
@@ -1208,6 +1212,8 @@ export default {
|
|||||||
const isNoReply = /^NO_REPLY$/i.test(trimmed) || /^HEARTBEAT_OK$/i.test(trimmed);
|
const isNoReply = /^NO_REPLY$/i.test(trimmed) || /^HEARTBEAT_OK$/i.test(trimmed);
|
||||||
const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : "";
|
const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : "";
|
||||||
const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar);
|
const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar);
|
||||||
|
const waitId = live.waitIdentifier || "👤";
|
||||||
|
const hasWaitIdentifier = !!lastChar && lastChar === waitId;
|
||||||
const wasNoReply = isEmpty || isNoReply;
|
const wasNoReply = isEmpty || isNoReply;
|
||||||
|
|
||||||
const turnDebug = getTurnDebugInfo(channelId);
|
const turnDebug = getTurnDebugInfo(channelId);
|
||||||
@@ -1215,6 +1221,17 @@ export default {
|
|||||||
`dirigent: DEBUG turn state channel=${channelId} turnOrder=${JSON.stringify(turnDebug.turnOrder)} currentSpeaker=${turnDebug.currentSpeaker} noRepliedThisCycle=${JSON.stringify([...turnDebug.noRepliedThisCycle])}`,
|
`dirigent: DEBUG turn state channel=${channelId} turnOrder=${JSON.stringify(turnDebug.turnOrder)} currentSpeaker=${turnDebug.currentSpeaker} noRepliedThisCycle=${JSON.stringify([...turnDebug.noRepliedThisCycle])}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Wait identifier: agent wants a human reply → all agents go silent
|
||||||
|
if (hasWaitIdentifier) {
|
||||||
|
setWaitingForHuman(channelId);
|
||||||
|
sessionAllowed.delete(key);
|
||||||
|
sessionTurnHandled.add(key);
|
||||||
|
api.logger.info(
|
||||||
|
`dirigent: before_message_write wait-for-human triggered session=${key} channel=${channelId} accountId=${accountId}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const wasAllowed = sessionAllowed.get(key);
|
const wasAllowed = sessionAllowed.get(key);
|
||||||
|
|
||||||
if (wasNoReply) {
|
if (wasNoReply) {
|
||||||
@@ -1323,6 +1340,8 @@ export default {
|
|||||||
const isNoReply = /^NO_REPLY$/i.test(trimmed) || /^HEARTBEAT_OK$/i.test(trimmed);
|
const isNoReply = /^NO_REPLY$/i.test(trimmed) || /^HEARTBEAT_OK$/i.test(trimmed);
|
||||||
const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : "";
|
const lastChar = trimmed.length > 0 ? Array.from(trimmed).pop() || "" : "";
|
||||||
const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar);
|
const hasEndSymbol = !!lastChar && policy.endSymbols.includes(lastChar);
|
||||||
|
const waitId = live.waitIdentifier || "👤";
|
||||||
|
const hasWaitIdentifier = !!lastChar && lastChar === waitId;
|
||||||
const wasNoReply = isEmpty || isNoReply;
|
const wasNoReply = isEmpty || isNoReply;
|
||||||
|
|
||||||
// Skip if turn was already advanced in before_message_write
|
// Skip if turn was already advanced in before_message_write
|
||||||
@@ -1334,6 +1353,15 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait identifier detection (fallback if not caught in before_message_write)
|
||||||
|
if (hasWaitIdentifier) {
|
||||||
|
setWaitingForHuman(channelId);
|
||||||
|
api.logger.info(
|
||||||
|
`dirigent: message_sent wait-for-human triggered channel=${channelId} from=${accountId}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (wasNoReply || hasEndSymbol) {
|
if (wasNoReply || hasEndSymbol) {
|
||||||
const nextSpeaker = onSpeakerDone(channelId, accountId, wasNoReply);
|
const nextSpeaker = onSpeakerDone(channelId, accountId, wasNoReply);
|
||||||
const trigger = wasNoReply ? (isEmpty ? "empty" : "no_reply_keyword") : "end_symbol";
|
const trigger = wasNoReply ? (isEmpty ? "empty" : "no_reply_keyword") : "end_symbol";
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"bypassUserIds": { "type": "array", "items": { "type": "string" }, "default": [] },
|
"bypassUserIds": { "type": "array", "items": { "type": "string" }, "default": [] },
|
||||||
"endSymbols": { "type": "array", "items": { "type": "string" }, "default": ["🔚"] },
|
"endSymbols": { "type": "array", "items": { "type": "string" }, "default": ["🔚"] },
|
||||||
"schedulingIdentifier": { "type": "string", "default": "➡️" },
|
"schedulingIdentifier": { "type": "string", "default": "➡️" },
|
||||||
|
"waitIdentifier": { "type": "string", "default": "👤" },
|
||||||
"noReplyProvider": { "type": "string" },
|
"noReplyProvider": { "type": "string" },
|
||||||
"noReplyModel": { "type": "string" },
|
"noReplyModel": { "type": "string" },
|
||||||
"enableDiscordControlTool": { "type": "boolean", "default": true },
|
"enableDiscordControlTool": { "type": "boolean", "default": true },
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export type DirigentConfig = {
|
|||||||
endSymbols?: string[];
|
endSymbols?: string[];
|
||||||
/** Scheduling identifier sent by moderator to activate agents (default: ➡️) */
|
/** Scheduling identifier sent by moderator to activate agents (default: ➡️) */
|
||||||
schedulingIdentifier?: string;
|
schedulingIdentifier?: string;
|
||||||
|
/** Wait identifier: agent ends with this when waiting for a human reply (default: 👤) */
|
||||||
|
waitIdentifier?: string;
|
||||||
noReplyProvider: string;
|
noReplyProvider: string;
|
||||||
noReplyModel: string;
|
noReplyModel: string;
|
||||||
/** Discord bot token for the moderator bot (used for turn handoff messages) */
|
/** Discord bot token for the moderator bot (used for turn handoff messages) */
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export type ChannelTurnState = {
|
|||||||
savedTurnOrder?: string[];
|
savedTurnOrder?: string[];
|
||||||
/** First agent in override cycle; used to detect cycle completion */
|
/** First agent in override cycle; used to detect cycle completion */
|
||||||
overrideFirstAgent?: string;
|
overrideFirstAgent?: string;
|
||||||
|
// ── Wait-for-human state ──
|
||||||
|
/** When true, an agent used the wait identifier — all agents should stay silent until a human speaks */
|
||||||
|
waitingForHuman: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const channelTurns = new Map<string, ChannelTurnState>();
|
const channelTurns = new Map<string, ChannelTurnState>();
|
||||||
@@ -64,6 +67,7 @@ export function initTurnOrder(channelId: string, botAccountIds: string[]): void
|
|||||||
currentSpeaker: null, // start dormant
|
currentSpeaker: null, // start dormant
|
||||||
noRepliedThisCycle: new Set(),
|
noRepliedThisCycle: new Set(),
|
||||||
lastChangedAt: Date.now(),
|
lastChangedAt: Date.now(),
|
||||||
|
waitingForHuman: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +84,11 @@ export function checkTurn(channelId: string, accountId: string): {
|
|||||||
return { allowed: true, currentSpeaker: null, reason: "no_turn_state" };
|
return { allowed: true, currentSpeaker: null, reason: "no_turn_state" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Waiting for human → block all agents
|
||||||
|
if (state.waitingForHuman) {
|
||||||
|
return { allowed: false, currentSpeaker: null, reason: "waiting_for_human" };
|
||||||
|
}
|
||||||
|
|
||||||
// Not in turn order (human or unknown) → always allowed
|
// Not in turn order (human or unknown) → always allowed
|
||||||
if (!state.turnOrder.includes(accountId)) {
|
if (!state.turnOrder.includes(accountId)) {
|
||||||
return { allowed: true, currentSpeaker: state.currentSpeaker, reason: "not_in_turn_order" };
|
return { allowed: true, currentSpeaker: state.currentSpeaker, reason: "not_in_turn_order" };
|
||||||
@@ -122,7 +131,8 @@ export function onNewMessage(channelId: string, senderAccountId: string | undefi
|
|||||||
if (!state || state.turnOrder.length === 0) return;
|
if (!state || state.turnOrder.length === 0) return;
|
||||||
|
|
||||||
if (isHuman) {
|
if (isHuman) {
|
||||||
// Human message without @mentions: restore original order if overridden, activate from first
|
// Human message: clear wait-for-human, restore original order if overridden, activate from first
|
||||||
|
state.waitingForHuman = false;
|
||||||
restoreOriginalOrder(state);
|
restoreOriginalOrder(state);
|
||||||
state.currentSpeaker = state.turnOrder[0];
|
state.currentSpeaker = state.turnOrder[0];
|
||||||
state.noRepliedThisCycle = new Set();
|
state.noRepliedThisCycle = new Set();
|
||||||
@@ -130,6 +140,11 @@ export function onNewMessage(channelId: string, senderAccountId: string | undefi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.waitingForHuman) {
|
||||||
|
// Waiting for human — ignore non-human messages
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (state.currentSpeaker !== null) {
|
if (state.currentSpeaker !== null) {
|
||||||
// Already active, no change needed from incoming message
|
// Already active, no change needed from incoming message
|
||||||
return;
|
return;
|
||||||
@@ -204,6 +219,27 @@ export function hasMentionOverride(channelId: string): boolean {
|
|||||||
return !!state?.savedTurnOrder;
|
return !!state?.savedTurnOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the channel to "waiting for human" state.
|
||||||
|
* All agents will be routed to no-reply until a human sends a message.
|
||||||
|
*/
|
||||||
|
export function setWaitingForHuman(channelId: string): void {
|
||||||
|
const state = channelTurns.get(channelId);
|
||||||
|
if (!state) return;
|
||||||
|
state.waitingForHuman = true;
|
||||||
|
state.currentSpeaker = null;
|
||||||
|
state.noRepliedThisCycle = new Set();
|
||||||
|
state.lastChangedAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the channel is waiting for a human reply.
|
||||||
|
*/
|
||||||
|
export function isWaitingForHuman(channelId: string): boolean {
|
||||||
|
const state = channelTurns.get(channelId);
|
||||||
|
return !!state?.waitingForHuman;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the current speaker finishes (end symbol detected) or says NO_REPLY.
|
* Called when the current speaker finishes (end symbol detected) or says NO_REPLY.
|
||||||
* @param wasNoReply - true if the speaker said NO_REPLY (empty/silent)
|
* @param wasNoReply - true if the speaker said NO_REPLY (empty/silent)
|
||||||
@@ -288,6 +324,7 @@ export function resetTurn(channelId: string): void {
|
|||||||
restoreOriginalOrder(state);
|
restoreOriginalOrder(state);
|
||||||
state.currentSpeaker = null;
|
state.currentSpeaker = null;
|
||||||
state.noRepliedThisCycle = new Set();
|
state.noRepliedThisCycle = new Set();
|
||||||
|
state.waitingForHuman = false;
|
||||||
state.lastChangedAt = Date.now();
|
state.lastChangedAt = Date.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,6 +343,7 @@ export function getTurnDebugInfo(channelId: string): Record<string, unknown> {
|
|||||||
noRepliedThisCycle: [...state.noRepliedThisCycle],
|
noRepliedThisCycle: [...state.noRepliedThisCycle],
|
||||||
lastChangedAt: state.lastChangedAt,
|
lastChangedAt: state.lastChangedAt,
|
||||||
dormant: state.currentSpeaker === null,
|
dormant: state.currentSpeaker === null,
|
||||||
|
waitingForHuman: state.waitingForHuman,
|
||||||
hasOverride: !!state.savedTurnOrder,
|
hasOverride: !!state.savedTurnOrder,
|
||||||
overrideFirstAgent: state.overrideFirstAgent || null,
|
overrideFirstAgent: state.overrideFirstAgent || null,
|
||||||
savedTurnOrder: state.savedTurnOrder || null,
|
savedTurnOrder: state.savedTurnOrder || null,
|
||||||
|
|||||||
Reference in New Issue
Block a user