nanocoai/nanoclaw · error
engage_mode '${w.engage_mode}' can never engage on channel '
Error message
engage_mode '${w.engage_mode}' can never engage on channel '${channelKey}' — its adapter declares mentions: 'never' (no mention signal is emitted; use --engage-mode pattern) What it means
walkPluginDir lstats the plugin root and requires it to be a real directory that is not a symbolic link. A symlinked root, a regular file, or anything else throws before any content is read — part of the containment gate against hostile trees.
Source
Thrown at src/channels/channel-defaults.ts:207
if (!honored) {
throw new Error(
`session_mode 'per-thread' requires honored thread ids, but this wiring's thread policy resolves off ` +
(explicit
? `(explicit threads=false)`
: `(threads is NULL and channel '${key}' declares threads: false for its ${mg.is_group === 1 ? 'group' : 'dm'} context)`) +
` — set --threads true, or keep session_mode 'shared'`,
);
}
}
if (w.engage_mode !== 'mention' && w.engage_mode !== 'mention-sticky') return;
const channelKey = mg.instance ?? mg.channel_type;
if (!hasDeclaredChannelDefaults(channelKey, mg.channel_type)) return;
const decl = getChannelDefaults(channelKey, mg.channel_type);
if (decl.mentions === 'never') {
throw new Error(
`engage_mode '${w.engage_mode}' can never engage on channel '${channelKey}' — its adapter declares mentions: 'never' (no mention signal is emitted; use --engage-mode pattern)`,
);
}
if (w.engage_mode === 'mention-sticky') {
const ctx = mg.is_group === 1 ? decl.group : decl.dm;
const threads = w.threads === undefined || w.threads === null ? ctx.threads : w.threads !== 0;
if (!threads) {
log.warn('mention-sticky requires thread ids — coerced to mention', {
channel: channelKey,
messagingGroupId: mg.id,
});
w.engage_mode = 'mention';
}
}
}
View on GitHub (pinned to 294ef2aee8)
Solutions
- Replace the symlink with the real directory (or a copy/bind) at the expected path
- If passing a file, point at the extracted plugin directory instead
- Re-clone or re-extract the plugin so the root is a regular directory
Defensive patterns
Strategy: validation
Validate before calling
const st = fs.lstatSync(dir);
if (st.isSymbolicLink() || !st.isDirectory()) {
// materialize a real directory before walkPluginDir
} Type guard
function isRegularDir(p: string): boolean {
const st = fs.lstatSync(p);
return st.isDirectory() && !st.isSymbolicLink();
} Try / catch
try { walkPluginDir(dir); } catch (e) { if (e instanceof Error && e.message.includes('must be a regular directory')) { /* copy to a real dir and retry */ } else throw e; } Prevention
- Never manage plugin checkouts via top-level symlinks; use git worktrees or clones
- Materialize extracted archives to plain directories
When it happens
Trigger: Passing a path that is a symlink to the real plugin directory, or a path that is a file; root existing but not as a plain directory (e.g. fifo).
Common situations: Templates managed via symlinks into a shared checkout; 'ln -s' used to alias a plugin name; passing a packaged .zip/.json file path instead of the extracted directory.
Related errors
- chat-sdk bridge instance ${JSON.stringify(config.instance)}
- no verb "${verbArg}" on ${res.plural} — run `ncl ${res.plura
- --${column.name.replace(/_/g, '-')} must be a number
- ${NANOCLAW_EXTENSION_NS}/context/instructions.md must be a r
- ${settingsFile} hooks.SessionStart must be an array
AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28).
Data as JSON: /api/errors/ed6754f767d57e9c.
Report an issue: GitHub.