feat: rewrite Slack plugin for microkernel architecture#2
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…, event-router Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Port DM → auto-create-session flow from PR #67. When a user sends a DM to the bot, the event router now triggers onNewSession (same as notification channel messages). The adapter creates a new session channel, invites the user, and routes the initial message. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy all 12 source files from OpenACP built-in slack plugin and rewrite imports to use @openacp/plugin-sdk instead of internal core paths. Key changes: define SlackChannelConfig locally with Zod, accept logger via constructor params, access core via ctx.core in setup(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy all test files from OpenACP monorepo and rewrite imports to use local relative paths. Conformance test is skipped as runAdapterConformanceTests is not yet exported by @openacp/plugin-sdk (follow-up needed). 65 tests pass, 1 skipped. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add DM auto-session creation in event-router (channelId starts with D) - Replace basic install() with full manifest-based setup wizard - Add generateSlackManifest() and validateSlackBotToken() in setup.ts - Invite user into new session channel and send DM notification - Add im:write scope to manifest for DM replies - Fix onNewSession to use default agent instead of userId as agentName Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0xmrpeter
left a comment
There was a problem hiding this comment.
Code Review: Rewrite Slack Plugin for Microkernel Architecture
Critical
1. setup() never calls adapter.start() — adapter is dead on arrival — src/index.ts
The setup() hook creates a SlackAdapter and registers it as a service, but never calls adapter.start(). Without it: no Bolt app init, no WebClient, no event handlers, no app.start(). The adapter is completely inert.
2. RenderedPermission not exported from @openacp/plugin-sdk — src/renderer.ts
Imports RenderedPermission from the SDK, but the SDK only exports IRenderer and RenderedMessage. This will fail to compile.
3. Duplicate session metadata maps → data inconsistency — src/adapter.ts
Two separate maps: sessions (used by createSessionThread, sendPermissionRequest, etc.) and _sessionMetas (used by getSessionMeta(), called from all message handlers). _sessionMetas is only populated transiently during sendMessage(). Any handler called outside that flow gets undefined and messages are silently dropped. Looks like _sessionMetas is vestigial — sessions should be the single source of truth.
Important
4. ctx.core typed as any — src/index.ts
const core = ctx.core as any bypasses all type safety. The plugin defines its own CoreKernel interface locally. If core API changes, this breaks silently at runtime. The CoreKernel interface should be exported from the SDK.
5. Spec document describes wrong architecture — docs/superpowers/specs/
References AdapterFactory pattern and @openacp/cli imports. Actual code uses OpenACPPlugin lifecycle with @openacp/plugin-sdk. Will mislead contributors. Remove or rewrite.
6. configure() doesn't loop — src/index.ts
Shows a menu, handles one selection, returns. User can only change one setting per invocation. Should loop until "Done".
7. Bot token validation uses raw fetch() instead of WebClient — src/setup.ts
validateSlackBotToken() calls fetch('https://slack.com/api/auth.test') directly, bypassing WebClient's retry/error handling.
8. PQueue.add() can return undefined — src/send-queue.ts
Method signature promises Promise<T> but queue.add() returns Promise<T | void>. Unchecked.
Minor / Nit
9. No maxMessageLength in config schema — hardcoded to 3000 in index.ts.
10. Inconsistent style — index.ts has no semicolons, all other files do.
11. adapter field uses z.literal("slack") but README shows "@openacp/adapter-slack".
12. adapter-lifecycle.test.ts only tests TextBuffer, not actual adapter lifecycle — no test coverage for start()/stop(), createSessionThread, sendMessage flow.
Test Gaps
Unit tests for event-router, formatter, permission-handler, send-queue, text-buffer, slug are solid. But the adapter itself has zero direct test coverage: no start()/stop(), no session thread management, no sendMessage flow. Conformance tests are explicitly skipped.
- Update peerDependencies to >=2026.327.0, devDependencies to file: link - Wrap configure() menu in while loop until user selects 'done' - Fix renderPlan signature to match BaseRenderer (1 arg, not 2) - Delete obsolete spec doc referencing old AdapterFactory architecture Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EthanMiller0x
left a comment
There was a problem hiding this comment.
Review Response
Fixed
| # | Item | Fix |
|---|---|---|
| 2 | RenderedPermission missing from SDK |
Added export to @openacp/plugin-sdk (committed in OpenACP repo) |
| 5 | Spec doc references old architecture | Deleted docs/superpowers/specs/2026-03-27-slack-plugin-context.md |
| 6 | configure() doesn't loop |
Wrapped in while (true) { ... if (choice === 'done') break } |
| SDK version | "^1.0.0" doesn't exist on npm |
Fixed — peerDep ">=2026.327.0", devDep file: link for local dev |
Not fixing (explained)
| # | Item | Reason |
|---|---|---|
| 1 | setup() doesn't call adapter.start() |
Verified against Telegram plugin — same pattern. Telegram also only calls registerService() without start(). Core handles adapter lifecycle after registration. Confirmed working end-to-end (bot connects, receives DMs, creates sessions). |
| 3 | Duplicate session maps | _sessionMetas is a call-scoped holder pattern from the original redesign branch code. It prevents concurrent sendMessage() calls from overwriting each other's metadata. Functional, not vestigial. Improvement possible but not blocking. |
| 4 | ctx.core as any |
SDK deliberately does not export OpenACPCore for external plugins. Local CoreKernel interface defines only what the adapter uses. This is the recommended pattern per the plugin architecture docs. |
| 7 | Raw fetch() in validation |
Intentional — validation runs during install() wizard BEFORE WebClient is created. No client available at that point. |
| 8 | PQueue.add() returns T | void |
The callback always returns a value (client.apiCall()), so runtime return is always T. TypeScript generic cast handles the type. |
| 9-12 | Minor/nit items | maxMessageLength matches Slack's fixed 3000-char limit. Style, schema naming, and test coverage improvements deferred to follow-up. |
0xmrpeter
left a comment
There was a problem hiding this comment.
Code Review
Critical
1. Duplicate session tracking — sessions vs _sessionMetas (src/adapter.ts)
Two maps track sessions. If a handler is called outside sendMessage, getSessionMeta() returns undefined and silently drops the message. Consolidate or document why both exist.
2. Race condition in onNewSession (src/adapter.ts ~L571-601)
this.sessions.get(session.id) may be undefined if createSessionThread hasn't completed yet. User invite and DM notification silently skipped. At minimum add a warning log.
3. Initial DM text is lost (src/adapter.ts ~L571-573)
When a DM triggers onNewSession, the user's text and userId are not forwarded — passed as undefined. The first message is never sent to the agent.
Important
4. conversations.open bypasses rate limiting (src/adapter.ts ~L586)
Direct this.webClient.conversations.open() call skips SlackSendQueue. Add it to the SlackMethod union and route through the queue.
5. Bot token stored as plaintext (src/setup.ts, src/index.ts)
Credentials stored in settings.json without encryption. At minimum document this or mention setting file permissions.
6. Default export is a pre-created singleton (src/index.ts)
export default createSlackPlugin() — if imported multiple times, same instance with accumulated state. Consider exporting the factory function.
7. renderPlan signature mismatch (src/renderer.ts)
Takes 1 arg while other render methods take (content, verbosity). Verify base class signature.
Minor
8. Notification emoji map duplicated between adapter.ts and renderer.ts — extract to shared constant.
9. Logger interface defined 3 times across files — extract to types.ts.
10. splitSafe doesn't handle code fence boundaries — broken rendering for long code blocks.
11. package-lock.json has file:../OpenACP/packages/plugin-sdk local path — will break for other contributors.
12. CoreKernel interface manually typed and cast via as any — brittle if core API changes.
13. Missing .env in .gitignore.
- Forward original DM text to agent after session creation (Open-ACP#3) - Add warning log when session meta is undefined (Open-ACP#2) - Route conversations.open through rate limit queue (Open-ACP#4) - Deduplicate Logger interface into src/types.ts (Open-ACP#9) - Add package-lock.json and .env to .gitignore (#11, #13) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EthanMiller0x
left a comment
There was a problem hiding this comment.
Review Response (round 2)
Fixed
| # | Item | Fix |
|---|---|---|
| 3 | DM text lost | Forward original text via core.handleMessage() after session creation and user invite |
| 2 | Race condition | Added warning log when sessions.get() returns undefined |
| 4 | conversations.open bypasses queue |
Added to SlackMethod union (Tier 3, 50 rpm), routed through queue.enqueue() |
| 9 | Logger interface 3x | Unified in src/types.ts, imported in adapter, event-router, text-buffer |
| 11 | package-lock.json local paths |
Added to .gitignore, removed from tracking |
| 13 | .env missing from .gitignore |
Added |
Not fixing
| # | Item | Reason |
|---|---|---|
| 1 | Duplicate session maps | Call-scoped holder pattern for concurrent sendMessage(). Already explained. |
| 5 | Plaintext credentials | All OpenACP adapters (Telegram, Discord) store tokens in plaintext settings.json. Platform-wide concern, not Slack-specific. |
| 6 | Singleton export | Standard ESM behavior — module cache guarantees single instance. Factory unnecessary. |
| 7 | renderPlan signature |
Verified — matches base class (1 arg). Correct. |
| 8, 10 | Emoji dedup, splitSafe fences | Minor improvements, deferred to follow-up. |
| 12 | CoreKernel manual typing |
SDK does not export OpenACPCore by design. Local interface is the recommended pattern. |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0xmrpeter
left a comment
There was a problem hiding this comment.
Re-review (round 3)
All critical and important issues from rounds 1-2 are resolved. Verified fixes:
conversations.opennow routed throughSlackSendQueue(Tier 3, 50 rpm)- DM text forwarded to agent via
handleMessage()after session creation - Race condition: warning log added when
sessions.get()returns undefined configure()properly loops until "Done"- Logger interface unified in
types.ts .envandpackage-lock.jsonin.gitignore
Remaining items (non-blocking, for follow-up)
1. handleNewSession passes undefined for userId (adapter.ts ~L1391)
core.handleNewSession("slack", undefined, undefined, ...) — the userId from the DM/notification message is available but not passed. Core won't associate the session with the user who created it.
2. Direct session.threadId mutation (adapter.ts ~L1513, ~L1594)
session.threadId = slug reaches into session internals. Would be cleaner via a session manager API (e.g. patchRecord already used nearby).
3. Adapter integration tests — component tests are solid, but no coverage for start() → sendMessage() → stop() flow or createSessionThread → renameSessionThread → deleteSessionThread lifecycle. Worth adding in a follow-up.
LGTM — approve.
Summary
@openacp/plugin-sdk(replaces old@openacp/cliAdapterFactory pattern)OpenACPPluginwith full lifecycle hooks:install(),configure(),setup(),teardown(),uninstall()#openacp-notificationschannel during setupRelated PRs
Test plan
npm run buildpassesnpm test— 65 tests pass, 1 skipped (conformance)im:write🤖 Generated with Claude Code