OpenCode Session Pattern

Version: 1.0
Date: 2026-02-12
Purpose: Shared reference for chat-accessible OpenCode sessions


Overview

This document describes the canonical pattern for integrating OpenCode with chat platforms (Slack, WhatsApp, Discord, etc.) using builder workspaces and bidirectional communication.

What This Covers

  • Multi-tier architecture (DM → Personal Assistant → Orchestrator → Builders)
  • Orchestrator session (workspace-level operations and project management)
  • Builder workspace creation (sibling worktree pattern)
  • OpenCode session lifecycle
  • Message routing (chat ↔ OpenCode)
  • Session management (create, pause, resume, terminate)
  • Integration with .ai/projectlist.md (projects as sessions)

What This Doesn’t Cover

  • Chat platform specifics (Slack webhooks, WhatsApp message format, etc.)
  • Gateway implementation details (these are adapters)
  • Deployment automation (namespace creation, ConfigMaps, etc.)

Architecture Tiers

Tier 1: DM Channel (Chat Interface)

Platform binding (WhatsApp DM, Slack DM, etc.) that routes user messages to the Personal Assistant.

Tier 2: Personal Assistant (OpenClaw Agent)

General-purpose conversational AI with access to OpenClaw tools (browser, message, etc.).

Responsibilities:

  • General conversation, questions, web searches
  • Update .ai/projectlist.md status (with human approval)
  • Query and display session status
  • Delegate workspace operations to Orchestrator
  • Non-workspace operations

Tier 3: Orchestrator (OpenCode @ workspace-root)

Special OpenCode session running at workspace root that manages workspace-level operations.

Location: Runs directly at <workspace-root>/ (no builder workspace needed)

Access: Full workspace access (.ai/, repos/, codev/, .builders/, Taskfile.yaml)

Deployment: Always-running daemon (existing OpenCode web pod in code-server namespace)

Responsibilities:

  • Create builder sessions (via Taskfile builder:init)
  • Terminate builder sessions (via Taskfile builder:cleanup)
  • Sync builder workspace state back to repos
  • Read .ai/projectlist.md to discover projects
  • Reconcile builders based on projectlist changes

Does NOT:

  • Update .ai/projectlist.md (Personal Assistant handles this)
  • Route messages to builders (Gateway handles this via chat bindings)

Tier 4: Builder Sessions (OpenCode @ .builders/)

Project-specific coding sessions in isolated worktree workspaces.

Created by: Orchestrator (delegated from Personal Assistant)

Bound to: Chat channels/groups/threads via Gateway

Naming convention: .builders/<project-id>-<name>/ (from .ai/projectlist.md)


Core Concepts

1. Builder Workspace

A builder workspace is an isolated environment where OpenCode runs with access to:

  • workspace-root files (specs, docs, tasks)
  • One or more project repos (the code being worked on)

Structure:

<workspace-root>/.builders/<builder-name>/
├── workspace-root/   (worktree → <workspace-root>)
├── repo-a/           (worktree → <workspace-root>/repos/repo-a)
└── repo-b/           (worktree → <workspace-root>/repos/repo-b)

Why sibling worktrees:

  • ✅ No gitignore conflicts (siblings, not nested under repos/)
  • ✅ Each worktree has its own .git and tracking
  • ✅ OpenCode sees all repos + workspace-root files
  • ✅ Clean isolation between parallel sessions

2. OpenCode Session

An OpenCode session is a running instance of the OpenCode agent (via opencode acp or opencode web).

Types:

Orchestrator Session

  • Location: Runs at workspace root (<workspace-root>/)
  • Access: Full workspace (all repos, .ai/, .builders/, etc.)
  • Lifecycle: Always-running (started with deployment, not per-project)
  • Purpose: Workspace-level operations and project management

Builder Session

  • Location: Runs in .builders/<project-id>-<name>/
  • Access: Scoped to builder workspace (worktrees only)
  • Lifecycle: Created per-project, terminated when done
  • Purpose: Project-specific coding tasks

Builder Session Properties:

  • session_id: Project ID from .ai/projectlist.md (e.g., "0014")
  • builder_name: <project-id>-<name> (e.g., "0014-ironman-training")
  • builder_path: .builders/<builder-name>/
  • repos: List of repos included as siblings (from projectlist.md)
  • model: LLM model (e.g., anthropic/claude-sonnet-4)
  • status: Mirrors project status in .ai/projectlist.md

3. Chat Binding

A chat binding links a builder session to a chat surface (thread, channel, group, DM).

Stored in:

  • .ai/projectlist.md - Project metadata and binding info
  • ~/.openclaw/config/ - Gateway configuration (must be correct)

Properties:

  • session_id: Project ID (e.g., "0014")
  • platform: slack, whatsapp, discord, etc.
  • channel_id: Platform-specific identifier (thread_ts, group JID, channel ID) - optional in projectlist
  • trigger: How messages are routed:
    • all_messages: Every message in channel → agent
    • mention: Only when agent is @mentioned
    • prefix: Only messages starting with command prefix
  • created_by: User who created the binding

Multi-binding: YAML supports multiple bindings per project (future-proofed, not currently in scope)

4. Project-Session Mapping

Key principle: Projects in .ai/projectlist.md ARE the sessions.

Mapping:

# In .ai/projectlist.md
- id: "0014"
  title: "Ironman Training"
  repos: [ai-dev]  # Which repos this project needs
  files:
    plan: .ai/projects/tri-training/2026-02-12IMBuild/plan.md
  # Project metadata path inferred from files.spec or files.plan directory

Results in:

  • Builder workspace: .builders/0014-ironman-training/
  • Session ID: "0014"
  • Project metadata: .ai/projects/tri-training/2026-02-12IMBuild/
  • Repos: Defined in projectlist.md, can be extended when creating builder

Lifecycle

Phase 0: Project Planning (Human)

Before any builders are created:

  1. Human updates .ai/projectlist.md:

    - id: "0015"
      title: "New Feature"
      status: planned  # Human sets this
      repos: [ai-dev, domain-apis]
      files:
        spec: .ai/projects/ai-dev/new-feature/prd.md
  2. Human tells Personal Assistant:

    "I want to start working on project 0015"
    
  3. Personal Assistant updates projectlist.md:

    status: implementing  # PA changes with human approval
  4. Personal Assistant delegates to Orchestrator:

    "Create builder for project 0015"
    

Phase 1: Creation (Orchestrator)

Orchestrator reads .ai/projectlist.md:

  • Project ID: 0015
  • Repos needed: [ai-dev, domain-apis]
  • Builder name: 0015-new-feature (derived from project title)

Orchestrator executes:

# Via Taskfile (orchestrator runs this)
task builder:init BUILDER_NAME=0015-new-feature REPOS=ai-dev,domain-apis
 
# Taskfile creates:
# 1. .builders/0015-new-feature/
# 2. Worktree for workspace-root
# 3. Worktree for ai-dev
# 4. Worktree for domain-apis
 
# Start OpenCode session in builder
cd .builders/0015-new-feature
opencode acp --cwd . --session 0015

Chat binding:

  • Stored in .ai/projectlist.md + ~/.openclaw/config/
  • Gateway routes messages from chat channel → builder session

Result:

  • ✅ Builder workspace at .builders/0015-new-feature/
  • ✅ OpenCode session running, session_id = 0015
  • ✅ Gateway knows to route chat channel → this session
  • ✅ Personal Assistant can send messages to builder via chat binding

Phase 2: Usage

Message Flow (Chat → Builder):

User sends message in WhatsApp group
    ↓
Gateway receives message
    ↓
Gateway looks up binding (channel → session)
    ↓
Gateway routes message to Builder OpenCode session (via ACP)
    POST /sessions/0015/messages
    Body: { "content": "<message>", "author": "<user>" }
    ↓
Builder OpenCode agent processes message
    (reads files, makes changes, runs commands)
    ↓
Builder streams events via WebSocket
    (file_opened, file_changed, command_run, etc.)

Event Flow (Builder → Chat):

Builder OpenCode emits event via WebSocket
    ↓
Gateway receives event
    ↓
Gateway filters events (hardcoded rules, no AI processing)
    - Keep: agent.complete, agent.question, agent.error
    - Drop: agent.file_opened, agent.thinking, etc.
    ↓
Gateway formats for chat platform
    ↓
Gateway sends to bound channel
    (WhatsApp group, Slack thread, etc.)

Event Filtering (Token Efficiency):

To avoid expensive AI processing and noisy chat, Gateway filters events using hardcoded rules:

Always forward:

  • agent.complete - ”✅ Done! Added rate limiting.”
  • agent.question - ”❓ Should I write tests?”
  • agent.error - ”❌ Build failed: missing dependency”
  • agent.waiting_approval - ”🔒 Need permission to run npm install”
  • agent.started - ”🔨 Working on it…” (immediate feedback)

Never forward:

  • agent.file_opened, agent.file_read, agent.thinking (too noisy)

Conditional:

  • agent.command_run - Only if exit_code !== 0 (failed commands)
  • agent.test_run - Only if tests failed

Session history storage: Full event stream stored for on-demand queries (implementation TBD: file-based in builder, ACP session history API, or other)

Example:

User (in chat): "Add rate limiting to /login endpoint"

OpenCode (via gateway → chat):
  📂 Opened ai-dev/services/gateway/src/auth.ts
  ✏️ Added express-rate-limit middleware
  🧪 Running tests...
  ✅ Tests passed!
  Done! Rate limiter added (5 req/min). Ready to commit?

User: "Yes, commit"

OpenCode:
  ✅ Committed: feat: add rate limiting to login
  🚀 Pushed to ai-dev/main

Phase 3: Management

Operations Split:

OperationHandlerDetails
List sessionsPersonal AssistantReads .ai/projectlist.md, shows projects with status: implementing
Show session statusPersonal AssistantQueries stored metadata, displays to user
Update project statusPersonal AssistantUpdates .ai/projectlist.md with human approval
Create builderOrchestratorReads projectlist, runs task builder:init
Terminate builderOrchestratorRuns task builder:cleanup, checks for uncommitted changes
Sync workspaceOrchestratorPushes changes from builder back to repos
Route messagesGatewayVia chat bindings (not Personal Assistant)

Example (WhatsApp DM):

User (DM): "Show me all OpenCode sessions"

Personal Assistant reads .ai/projectlist.md:
  📊 Active Sessions:
  
  1. Project 0014: Ironman Training
     Status: implementing
     Repos: ai-dev
     Builder: .builders/0014-ironman-training
     Platform: WhatsApp group "Coach"
  
  2. Project 0015: New Feature
     Status: implementing
     Repos: ai-dev, domain-apis
     Builder: .builders/0015-new-feature
     Platform: WhatsApp group "Dev Team"

User: "I want to wrap up project 0014"

Personal Assistant: "Ready to update status to 'implemented'?"
User: "Yes"

Personal Assistant updates .ai/projectlist.md:
  status: implemented

Personal Assistant: "Updated! Ready to terminate the builder?"
User: "Yes"

Personal Assistant → Orchestrator: "Terminate builder 0014-ironman-training"
Orchestrator: Checks for uncommitted changes, runs task builder:cleanup

Personal Assistant: "✅ Builder cleaned up. Project 0014 is now 'implemented'."

Phase 4: Cleanup

Trigger:

  • Manual only (for now, designed to support automation later)
  • User explicitly requests termination via Personal Assistant
  • Project status changed to implemented, committed, integrated, or abandoned

Process:

  1. Personal Assistant asks Orchestrator to terminate builder:

    Personal Assistant → Orchestrator: "Terminate builder 0015-new-feature"
    
  2. Orchestrator validation:

    • Check for uncommitted changes in worktrees
    • If found: Report back to Personal Assistant → User must decide
    • If clean: Proceed with cleanup
  3. Orchestrator executes cleanup:

    task builder:cleanup BUILDER_NAME=0015-new-feature
  4. Taskfile cleanup steps:

    builder:cleanup:
      desc: Cleanup builder workspace and worktrees
      vars:
        BUILDER_NAME: '{{.BUILDER_NAME}}'
      cmds:
        - |
          cd .builders/{{.BUILDER_NAME}}
          
          # Check for uncommitted changes first
          if ! git diff --quiet || ! git diff --cached --quiet; then
            echo "ERROR: Uncommitted changes found"
            exit 1
          fi
          
          # Remove each worktree
          for dir in */; do
            (cd "$dir" && git worktree remove .)
          done
          
          cd ../..
          rm -rf .builders/{{.BUILDER_NAME}}
          git worktree prune
  5. Orchestrator reports back:

    Orchestrator → Personal Assistant: "Builder 0015-new-feature cleaned up successfully"
    
  6. Personal Assistant notifies user:

    "✅ Builder cleaned up. Project 0015 workspace removed."
    

What’s preserved:

  • Session history/logs (implementation TBD - may move to .ai/projects/.../session-history.json)
  • Git commits (assumed pushed to remote already)
  • Project metadata in .ai/projects/ (untouched)

What’s deleted:

  • Builder directory .builders/0015-new-feature/
  • All worktrees
  • Local uncommitted changes (cleanup blocked if present)

Shared Infrastructure

Workspace Root Volume

All components share a common workspace root volume (mounted at various locations depending on environment):

<workspace-root>/                    # Shared volume
├── repos/                           # Git repositories
│   ├── ai-dev/
│   ├── domain-apis/
│   └── k8s-lab/
│
├── .builders/                       # Builder workspaces (ephemeral)
│   ├── api-team-abc123/
│   ├── frontend-team-def456/
│   └── ...
│
├── .ai/                             # Specs, docs, notes
├── codev/                           # Codev artifacts
└── Taskfile.yaml                    # Workspace tasks

Why shared:

  • No file syncing needed
  • All components see the same files
  • Builder workspaces are just subdirectories
  • Worktrees reference repos in-place

Message Routing Strategies

Strategy 1: All Messages (Dedicated Channels)

Use case: Channel exists solely for one OpenCode session

Config:

{
  "trigger": "all_messages",
  "session_id": "abc123"
}

Behavior:

  • Every message in channel → OpenCode
  • Agent has full conversation context
  • Natural team collaboration

Example:

Alice: We need rate limiting
Bob: 5 requests per minute?
Alice: Yes

OpenCode: Got it, adding 5 req/min rate limit to /login...

Pros:

  • ✅ Natural conversation flow
  • ✅ Agent has full context
  • ✅ No @mention friction

Cons:

  • ❌ Burns tokens on every message
  • ❌ Not suitable for shared channels

Strategy 2: Mention-Based (Shared Channels)

Use case: Multi-purpose channel with multiple conversations

Config:

{
  "trigger": "mention",
  "mention_patterns": ["@opencode", "hey opencode"],
  "session_id": "abc123"
}

Behavior:

  • Only messages mentioning agent → OpenCode
  • Agent doesn’t see other conversations
  • Explicit invocation required

Example:

Alice: We need rate limiting
Bob: 5 requests per minute?
Alice: @opencode add rate limiting, 5 req/min

OpenCode: Adding 5 req/min rate limit to /login...

Pros:

  • ✅ Token-efficient
  • ✅ Works in busy channels
  • ✅ Clear intent

Cons:

  • ❌ Agent misses context from prior messages
  • ❌ Extra friction (@mention required)

Strategy 3: Prefix-Based

Use case: Command-style interaction

Config:

{
  "trigger": "prefix",
  "prefixes": ["/code", "!opencode"],
  "session_id": "abc123"
}

Behavior:

  • Only messages with prefix → OpenCode
  • Clear command syntax
  • Agent doesn’t see other messages

Example:

/code add rate limiting to /login
!opencode run tests

Pros:

  • ✅ Very explicit
  • ✅ Token-efficient
  • ✅ Familiar command pattern

Cons:

  • ❌ Less conversational
  • ❌ No context from prior messages

Platform Adapters

Each chat platform needs an adapter that:

  1. Receives messages from platform (webhooks, polling, etc.)
  2. Looks up binding (channel → session)
  3. Routes to OpenCode (via ACP)
  4. Receives events from OpenCode (WebSocket)
  5. Formats and sends to platform (API calls)

Example Adapters:

Slack Adapter

  • Receives: Slack Event API webhooks
  • Binding key: thread_ts (thread identifier)
  • Sends via: Slack Web API (chat.postMessage)

WhatsApp Adapter

  • Receives: WhatsApp Business API webhooks
  • Binding key: group_jid (group identifier)
  • Sends via: WhatsApp Business API

Discord Adapter

  • Receives: Discord Gateway WebSocket
  • Binding key: channel_id (channel identifier)
  • Sends via: Discord REST API

Common Interface:

interface ChatAdapter {
  // Receive message from platform
  onMessage(message: PlatformMessage): Promise<void>
  
  // Send message to platform
  sendMessage(channel: string, content: string): Promise<void>
  
  // Format OpenCode event for platform
  formatEvent(event: OpenCodeEvent): PlatformMessage
}

Configuration

Project Configuration (.ai/projectlist.md)

Projects are the source of truth for sessions:

projects:
  - id: "0014"
    title: "Ironman Training"
    status: implementing  # Personal Assistant updates this
    repos: [ai-dev]  # Which repos this project needs (can be extended)
    files:
      plan: .ai/projects/tri-training/2026-02-12IMBuild/plan.md
    chat_binding:  # Optional: chat binding metadata
      platform: whatsapp
      group_name: "Coach"
      trigger: all_messages
    tags: [ai-dev, openclaw, coaching, training, whatsapp]
    notes: "Builder: .builders/0014-ironman-training"

Gateway Configuration (~/.openclaw/config/)

Gateway maintains chat bindings (must be correct):

opencode:
  workspace_root: ${WORKSPACE_ROOT}  # Set via env var or mount point
  builders_dir: ${WORKSPACE_ROOT}/.builders
  
  # Orchestrator connection
  orchestrator:
    acp_url: http://opencode-web.code-server:8080  # Existing OpenCode web pod
    session_id: orchestrator  # Special orchestrator session
  
  # Default session settings
  defaults:
    model: anthropic/claude-sonnet-4
    trigger: all_messages
  
  # Active bindings (synced from projectlist.md)
  bindings:
    - session_id: "0014"  # Project ID
      builder_name: 0014-ironman-training
      builder_path: .builders/0014-ironman-training
      repos: [ai-dev]
      platform: whatsapp
      channel_id: "120363xxx@g.us"
      channel_name: "Coach"
      trigger: all_messages
      created_by: "+447403331966"
      created_at: "2026-02-12T08:00:00Z"
    
    - session_id: "0015"
      builder_name: 0015-new-feature
      builder_path: .builders/0015-new-feature
      repos: [ai-dev, domain-apis]
      platform: whatsapp
      channel_id: "120363yyy@g.us"
      channel_name: "Dev Team"
      trigger: all_messages
      created_by: "+447403331966"
      created_at: "2026-02-13T09:00:00Z"

Event Filtering Configuration

Hardcoded in Gateway (no AI processing):

// Gateway event filter
const FORWARD_EVENTS = {
  always: [
    'agent.complete',
    'agent.question', 
    'agent.error',
    'agent.waiting_approval',
    'agent.started'
  ],
  never: [
    'agent.file_opened',
    'agent.file_read',
    'agent.thinking'
  ],
  conditional: {
    'agent.command_run': (event) => event.exit_code !== 0,
    'agent.test_run': (event) => event.failed > 0
  }
};

Special Cases

Multiple Builders Per Project

Projects can have multiple simultaneous builders (e.g., separate microservices):

# Project 0012 in .ai/projectlist.md
- id: "0012"
  title: "OpenCode Slack Integration"
  repos: [ai-dev]
  notes: "Two builders: 0012-slack-gateway, 0012-bridge-plugin"

Builders:

.builders/
├── 0012-slack-gateway/      # First builder (Python/FastAPI)
└── 0012-bridge-plugin/      # Second builder (TypeScript)

Chat bindings:

  • If >1 simultaneous builder in a category, create new WhatsApp group for subsequent builder
  • Each builder has its own group/thread
  • Builders run independently (integration contract agreed beforehand)

Status: All builders share same project status in projectlist.md


Implementation Checklist

Phase 0: Research & Discovery

  • OpenCode ACP protocol discovery (endpoints, WebSocket events, session management)
  • Test orchestrator session at workspace root (does it work as expected?)
  • Research OpenClaw event filtering/aggregation capabilities
  • Determine how Personal Assistant invokes Orchestrator (API vs tool wrapper)
  • Session history storage options (file-based, ACP API, or other)

Phase 1: Core Pattern

  • Taskfile: builder:init (create sibling worktrees)
  • Taskfile: builder:cleanup (remove worktrees, check uncommitted changes)
  • Test manual builder creation via orchestrator
  • Verify OpenCode sees all siblings correctly
  • Document repos field in .ai/projectlist.md schema

Phase 2: Orchestrator Integration

  • Deploy orchestrator session at workspace root (existing OpenCode web pod)
  • Orchestrator can read .ai/projectlist.md
  • Orchestrator can create builders (calls task builder:init)
  • Orchestrator can terminate builders (calls task builder:cleanup)
  • Personal Assistant → Orchestrator communication pattern

Phase 3: Personal Assistant

  • Personal Assistant reads/updates .ai/projectlist.md
  • Personal Assistant delegates builder operations to orchestrator
  • Personal Assistant shows session status (query projectlist)
  • Personal Assistant handles status transitions with human approval

Phase 4: Gateway & Bindings

  • Chat binding storage (projectlist.md + ~/.openclaw/config)
  • Message routing (chat → builder via ACP)
  • Event filtering (hardcoded rules, no AI processing)
  • Event streaming (builder → chat via WebSocket)
  • Sync bindings from projectlist.md to Gateway config

Phase 5: Platform Adapters

  • WhatsApp adapter (webhooks, message sending)
  • Slack adapter (if needed)
  • Event formatting per platform
  • Platform-specific features (inline buttons, etc.)

Key Design Decisions

From design-questions.md review (2026-02-12):

  1. Orchestrator Location: Runs directly at workspace root (no builder workspace needed)
  2. Orchestrator Lifecycle: Always-running daemon (existing OpenCode web pod)
  3. Session-Project Mapping: Project ID = Session ID, inferred from projectlist.md
  4. Repos Discovery: Maintained in projectlist.md (defaults from project metadata, can extend)
  5. Status Updates: Personal Assistant updates projectlist.md (with human approval), then orchestrator reconciles
  6. Event Filtering: Hardcoded rules in Gateway (no AI processing to avoid token cost)
  7. Multi-Builder: Allowed per project, each gets own chat group
  8. Builder Cleanup: Manual only (designed for future automation)
  9. Chat Bindings: Stored in projectlist.md + ~/.openclaw/config
  10. Backward Compatibility: Existing manual builders supported, no migration needed

Open Questions / Research Needed

Phase 0 Prerequisites:

  1. OpenCode ACP protocol details (endpoints, WebSocket event types, session history API)
  2. OpenClaw event filtering/aggregation capabilities (can we do hardcoded filters in code?)
  3. Personal Assistant → Orchestrator invocation pattern (recommend API over CLI, but needs research)
  4. Session history storage strategy (file-based vs ACP API vs other)

To be determined during implementation:

  • Exact event types emitted by OpenCode (for filtering rules)
  • WebSocket reconnection handling
  • Error recovery patterns
  • Session state persistence across restarts

References

  • Sibling Worktree Pattern: Original design from opencode-slack-integration
  • Builder Workspace: Used in codev and opencode-slack
  • ACP Protocol: OpenCode’s Agent Client Protocol
  • Project List: .ai/projectlist.md - Source of truth for projects/sessions
  • Design Decisions: .ai/projects/ai-dev/openclaw-opencode-bridge/design-questions.md

Version History

  • 1.0 (2026-02-12): Initial version extracted from openclaw-opencode-bridge and opencode-slack-integration specs
  • 1.1 (2026-02-13): Added multi-tier architecture (Orchestrator pattern), integration with projectlist.md, event filtering, design decisions

Usage in Projects

This document is referenced by:

  • openclaw-opencode-bridge (WhatsApp integration) - .ai/projects/ai-dev/openclaw-opencode-bridge/
  • opencode-slack-integration (Slack integration) - .ai/projects/ai-dev/opencode-slack-integration/
  • Future: Discord, Telegram, etc. integrations

Each project adds:

  • Platform-specific adapter implementation
  • UI/UX conventions for that platform
  • Platform-specific features (e.g., Slack workflow forms, WhatsApp inline buttons)

Each project references this doc for:

  • Multi-tier architecture (DM → Personal Assistant → Orchestrator → Builders)
  • Builder workspace creation
  • OpenCode session lifecycle
  • Message routing patterns
  • Event filtering rules
  • Integration with .ai/projectlist.md
  • Shared infrastructure layout