openclaw - ✅(Solved) Fix bug: resolvePluginContractApiPath does not search dist/ subdirectory — Discord npm plugin secret contract never loaded [2 pull requests, 6 comments, 7 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#77241Fetched 2026-05-05 05:50:51
View on GitHub
Comments
6
Participants
7
Timeline
18
Reactions
3
Timeline (top)
commented ×6cross-referenced ×5referenced ×3subscribed ×2

resolvePluginContractApiPath() in channel-contract-api-BCpH8X6U.js only searches the plugin's rootDir directly for secret-contract-api.js / contract-api.js. Since npm packages conventionally compile output to a dist/ subdirectory, the function always returns null for any installed npm channel plugin — including @openclaw/discord. As a result, Discord channel tokens are never resolved into the runtime snapshot, and all Discord accounts silently fail to start on every gateway boot.

Error Message

No [discord] entries appear in gateway.log after starting channels and sidecars.... All Discord accounts show stopped, disconnected, error:not configured in openclaw channels status. Telegram (bundled plugin) starts normally. Error is silently swallowed unless OPENCLAW_DEBUG_CHANNEL_CONTRACT_API=1: } catch (error) { This issue also affects the silent error suppression: a failed loadPluginContractModule call is swallowed without any log output unless OPENCLAW_DEBUG_CHANNEL_CONTRACT_API=1. Logging a warning by default (or at least at debug level) would make this class of failure much easier to diagnose.

Root Cause

resolvePluginContractApiPath(rootDir) searches:

{rootDir}/secret-contract-api.js   ← does not exist
{rootDir}/contract-api.js          ← does not exist

But the actual contract file installed by @openclaw/[email protected] is at:

{rootDir}/dist/secret-contract-api.js   ← exists, exports collectRuntimeConfigAssignments

The function never checks dist/. It returns null, loadExternalChannelSecretContractFromRecord returns undefined, collectChannelConfigAssignments falls through without assigning any Discord SecretRefs to the resolver context, and all Discord tokens remain unresolved in the runtime snapshot.

Relevant code path:

collectConfigAssignments
  → collectChannelConfigAssignments
    → loadChannelSecretContractApi({ channelId: "discord", ... })
      → listChannelSecretContractRecords  ← finds the @openclaw/discord record correctly
        → loadExternalChannelSecretContractFromRecord(record)
          → resolvePluginContractApiPath(record.rootDir)  ← returns null
          → returns undefined  ← contract never loaded

Error is silently swallowed unless OPENCLAW_DEBUG_CHANNEL_CONTRACT_API=1:

} catch (error) {
  if (process.env.OPENCLAW_DEBUG_CHANNEL_CONTRACT_API === "1") { ... }
  // silent otherwise
}

Fix Action

Workaround

Create a re-export shim at the package root:

openclaw gateway stop
echo 'export { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./dist/secret-contract-api.js";' \
  > ~/.openclaw/npm/node_modules/@openclaw/discord/secret-contract-api.js
openclaw gateway start

This file is lost on openclaw plugins update @openclaw/discord and must be recreated.

PR fix notes

PR #77246: fix(secrets): search dist/ subdir in resolvePluginContractApiPath for npm-compiled channel plugins

Description (problem / solution / changelog)

Problem

resolvePluginContractApiPath() in src/secrets/channel-contract-api.ts only searched the plugin rootDir directly for secret-contract-api.* / contract-api.* files. npm packages conventionally compile TypeScript output to a dist/ subdirectory, so any installed npm channel plugin (including @openclaw/discord) that places its contract under dist/ was never discovered. The result: Discord channel tokens were never resolved into the runtime snapshot, and all Discord accounts silently failed to start on every gateway boot.

Fix

Extend resolvePluginContractApiPath to search [rootDir, rootDir/dist] for both contract file names. rootDir is still preferred — dist/ is only checked as a fallback.

Tests

  • Added regression test to channel-contract-api.external.test.ts: a plugin with only dist/secret-contract-api.cjs (no root-level file) is discovered and its secretTargetRegistryEntries are returned correctly.
  • All 3 external channel contract tests pass.

PRE-IMPLEMENT AUDIT

  1. Existing-helper: resolvePluginContractApiPath is private and has no dist-aware variant elsewhere. PASS.
  2. Shared-helper: Only one call site (loadExternalChannelSecretContractFromRecord). PASS.
  3. Rival scan: No rival PR found. PASS.

Fixes #77241.

🤖 Generated with Claude Code

Changed files

  • src/secrets/channel-contract-api.external.test.ts (modified, +48/-0)
  • src/secrets/channel-contract-api.ts (modified, +13/-8)

PR #77247: fix(plugins): resolve npm channel plugin contract files from dist/ subdirectory

Description (problem / solution / changelog)

Summary

Fixes #77241 — resolvePluginContractApiPath() does not search the dist/ subdirectory, causing all npm-installed channel plugins to silently fail to load their secret contracts.

Root Cause

resolvePluginContractApiPath(rootDir) only searches rootDir directly for secret-contract-api.{ext} and contract-api.{ext}. npm packages conventionally compile output to a dist/ subdirectory, so the function returns null for any npm-installed channel plugin (including @openclaw/discord). This means Discord accounts configured via SecretRefs silently fail to start.

Fix

Add dist/ as an additional search directory in resolvePluginContractApiPath, checked after the existing root-level search. This matches the pattern already used by public-surface-runtime.ts and bundled-channel-runtime.ts for other plugin artifact resolution.

Changes

  • src/secrets/channel-contract-api.ts: After exhausting root-level candidates, check path.join(rootDir, 'dist') for the same contract files
  • src/secrets/channel-contract-api.external.test.ts: Add test verifying contract discovery from dist/ subdirectory
  • CHANGELOG.md: Entry under Fixes

Testing

  • npx vitest run src/secrets/channel-contract-api.external.test.ts — 3/3 passed ✅
  • New test creates a temp dir with contract file in dist/ subdirectory and confirms loadChannelSecretContractApi resolves it correctly

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/secrets/channel-contract-api.external.test.ts (modified, +169/-0)
  • src/secrets/channel-contract-api.ts (modified, +15/-0)

Code Example

[delivery-recovery] Retry failed for delivery <id>: Discord bot token configured for account "<name>" is unavailable; resolve SecretRefs against the active runtime snapshot before using this account.

---

{rootDir}/secret-contract-api.js   ← does not exist
{rootDir}/contract-api.js          ← does not exist

---

{rootDir}/dist/secret-contract-api.js   ← exists, exports collectRuntimeConfigAssignments

---

collectConfigAssignments
  → collectChannelConfigAssignments
loadChannelSecretContractApi({ channelId: "discord", ... })
      → listChannelSecretContractRecords  ← finds the @openclaw/discord record correctly
loadExternalChannelSecretContractFromRecord(record)
resolvePluginContractApiPath(record.rootDir)  ← returns null
          → returns undefined  ← contract never loaded

---

} catch (error) {
  if (process.env.OPENCLAW_DEBUG_CHANNEL_CONTRACT_API === "1") { ... }
  // silent otherwise
}

---

openclaw gateway stop
echo 'export { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./dist/secret-contract-api.js";' \
  > ~/.openclaw/npm/node_modules/@openclaw/discord/secret-contract-api.js
openclaw gateway start
RAW_BUFFERClick to expand / collapse

Summary

resolvePluginContractApiPath() in channel-contract-api-BCpH8X6U.js only searches the plugin's rootDir directly for secret-contract-api.js / contract-api.js. Since npm packages conventionally compile output to a dist/ subdirectory, the function always returns null for any installed npm channel plugin — including @openclaw/discord. As a result, Discord channel tokens are never resolved into the runtime snapshot, and all Discord accounts silently fail to start on every gateway boot.

Environment

  • OpenClaw version: 2026.5.3
  • Platform: macOS (darwin 25.4.0)
  • Discord plugin: @openclaw/[email protected] (installed as npm plugin)

Steps to Reproduce

  1. Install @openclaw/discord as an npm plugin (openclaw plugins install @openclaw/discord)
  2. Configure Discord channel accounts in openclaw.json with keychain SecretRefs
  3. Start the gateway

Expected Behavior

Discord accounts start and connect. gateway.log shows [discord] [<account>] starting provider entries after starting channels and sidecars....

Actual Behavior

No [discord] entries appear in gateway.log after starting channels and sidecars.... All Discord accounts show stopped, disconnected, error:not configured in openclaw channels status. Telegram (bundled plugin) starts normally.

gateway.err.log contains only downstream errors from delivery-recovery attempts:

[delivery-recovery] Retry failed for delivery <id>: Discord bot token configured for account "<name>" is unavailable; resolve SecretRefs against the active runtime snapshot before using this account.

Root Cause

resolvePluginContractApiPath(rootDir) searches:

{rootDir}/secret-contract-api.js   ← does not exist
{rootDir}/contract-api.js          ← does not exist

But the actual contract file installed by @openclaw/[email protected] is at:

{rootDir}/dist/secret-contract-api.js   ← exists, exports collectRuntimeConfigAssignments

The function never checks dist/. It returns null, loadExternalChannelSecretContractFromRecord returns undefined, collectChannelConfigAssignments falls through without assigning any Discord SecretRefs to the resolver context, and all Discord tokens remain unresolved in the runtime snapshot.

Relevant code path:

collectConfigAssignments
  → collectChannelConfigAssignments
    → loadChannelSecretContractApi({ channelId: "discord", ... })
      → listChannelSecretContractRecords  ← finds the @openclaw/discord record correctly
        → loadExternalChannelSecretContractFromRecord(record)
          → resolvePluginContractApiPath(record.rootDir)  ← returns null
          → returns undefined  ← contract never loaded

Error is silently swallowed unless OPENCLAW_DEBUG_CHANNEL_CONTRACT_API=1:

} catch (error) {
  if (process.env.OPENCLAW_DEBUG_CHANNEL_CONTRACT_API === "1") { ... }
  // silent otherwise
}

Workaround

Create a re-export shim at the package root:

openclaw gateway stop
echo 'export { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./dist/secret-contract-api.js";' \
  > ~/.openclaw/npm/node_modules/@openclaw/discord/secret-contract-api.js
openclaw gateway start

This file is lost on openclaw plugins update @openclaw/discord and must be recreated.

Suggested Fix

Either:

  1. resolvePluginContractApiPath should also search dist/ — check {rootDir}/dist/secret-contract-api.js and {rootDir}/dist/contract-api.js in addition to the root
  2. Plugin registry should store rootDir pointing to dist/ — use the compiled output directory rather than the package root
  3. @openclaw/discord should ship a root-level secret-contract-api.js — add a re-export entry point so the package root has the expected file

Option 1 is the most robust general fix since it handles any npm channel plugin that follows the standard dist/ convention.

Additional Context

This issue also affects the silent error suppression: a failed loadPluginContractModule call is swallowed without any log output unless OPENCLAW_DEBUG_CHANNEL_CONTRACT_API=1. Logging a warning by default (or at least at debug level) would make this class of failure much easier to diagnose.

extent analysis

TL;DR

The most likely fix is to modify the resolvePluginContractApiPath function to search for secret-contract-api.js and contract-api.js in the dist/ subdirectory of the plugin's root directory.

Guidance

  • Modify the resolvePluginContractApiPath function to check for the contract API files in the dist/ subdirectory, in addition to the root directory.
  • Verify that the function returns the correct path to the contract API file by logging the result or using a debugger.
  • Consider adding a warning log message when the contract API file is not found, to make it easier to diagnose similar issues in the future.
  • If modifying the resolvePluginContractApiPath function is not feasible, create a re-export shim at the package root as a temporary workaround.

Example

function resolvePluginContractApiPath(rootDir) {
  const possiblePaths = [
    `${rootDir}/secret-contract-api.js`,
    `${rootDir}/contract-api.js`,
    `${rootDir}/dist/secret-contract-api.js`,
    `${rootDir}/dist/contract-api.js`
  ];

  for (const path of possiblePaths) {
    if (fs.existsSync(path)) {
      return path;
    }
  }

  return null;
}

Notes

The suggested fix assumes that the dist/ subdirectory is the standard location for compiled output in npm packages. If this is not the case, the fix may need to be adjusted accordingly.

Recommendation

Apply the workaround by modifying the resolvePluginContractApiPath function to search for the contract API files in the dist/ subdirectory, as this is the most robust general fix.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix bug: resolvePluginContractApiPath does not search dist/ subdirectory — Discord npm plugin secret contract never loaded [2 pull requests, 6 comments, 7 participants]