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

  1. Replace the symlink with the real directory (or a copy/bind) at the expected path
  2. If passing a file, point at the extracted plugin directory instead
  3. 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

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


AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28). Data as JSON: /api/errors/ed6754f767d57e9c. Report an issue: GitHub.