{"record":{"id":"ed6754f767d57e9c","repo":"nanocoai/nanoclaw","slug":"engage-mode-w-engage-mode-can-never-engage-on","errorCode":null,"errorMessage":"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)","messagePattern":"engage_mode '(.+?)' can never engage on channel '(.+?)' — its adapter declares mentions: 'never' \\(no mention signal is emitted; use --engage-mode pattern\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/channels/channel-defaults.ts","lineNumber":207,"sourceCode":"    if (!honored) {\n      throw new Error(\n        `session_mode 'per-thread' requires honored thread ids, but this wiring's thread policy resolves off ` +\n          (explicit\n            ? `(explicit threads=false)`\n            : `(threads is NULL and channel '${key}' declares threads: false for its ${mg.is_group === 1 ? 'group' : 'dm'} context)`) +\n          ` — set --threads true, or keep session_mode 'shared'`,\n      );\n    }\n  }\n\n  if (w.engage_mode !== 'mention' && w.engage_mode !== 'mention-sticky') return;\n\n  const channelKey = mg.instance ?? mg.channel_type;\n  if (!hasDeclaredChannelDefaults(channelKey, mg.channel_type)) return;\n\n  const decl = getChannelDefaults(channelKey, mg.channel_type);\n  if (decl.mentions === 'never') {\n    throw new Error(\n      `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)`,\n    );\n  }\n  if (w.engage_mode === 'mention-sticky') {\n    const ctx = mg.is_group === 1 ? decl.group : decl.dm;\n    const threads = w.threads === undefined || w.threads === null ? ctx.threads : w.threads !== 0;\n    if (!threads) {\n      log.warn('mention-sticky requires thread ids — coerced to mention', {\n        channel: channelKey,\n        messagingGroupId: mg.id,\n      });\n      w.engage_mode = 'mention';\n    }\n  }\n}\n","sourceCodeStart":189,"sourceCodeEnd":223,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/channels/channel-defaults.ts#L189-L223","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const st = fs.lstatSync(dir);\nif (st.isSymbolicLink() || !st.isDirectory()) {\n  // materialize a real directory before walkPluginDir\n}","typeGuard":"function isRegularDir(p: string): boolean {\n  const st = fs.lstatSync(p);\n  return st.isDirectory() && !st.isSymbolicLink();\n}","tryCatchPattern":"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; }","preventionTips":["Never manage plugin checkouts via top-level symlinks; use git worktrees or clones","Materialize extracted archives to plain directories"],"tags":["security","filesystem","symlink","templates"],"backgroundTag":"symlink-rejected","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}