Multi-agent roundtable discussion — topic-driven multi-round debate with convergence detection and conclusion generation
---
name: agent-roundtable
description: "Multi-agent roundtable discussion — topic-driven multi-round debate with convergence detection and conclusion generation"
version: 1.2.4
platforms: [linux, macos, windows]
metadata:
hermes:
tags: [discussion, multi-agent, collaboration, debate, roundtable]
related_skills: [kanban-worker, kanban-orchestrator]
openclaw:
requires:
env: []
bins: []
primaryEnv: null
emoji: "🤝"
homepage: "https://roundtable.izmw.me"
---
# Roundtable Discussion Skill
## Publishing to skill hubs
When preparing Roundtable for Hermes Skill Hub or OpenClaw/ClawHub, use the release checklist in `src/skills/references/skill-hub-publishing.md`. Key reminders: keep `src/skills/` self-contained and free of private team data, include both Hermes and OpenClaw metadata blocks in `SKILL.md`, check `hermes skills publish --help`, `clawhub publish --help`, and `clawhub whoami`, and gate real publishing on user confirmation of target account/repo.
## Overview
Enable multiple agents to participate in structured, multi-round discussions
around a topic. Each participant is a **real sub-agent** spawned via
`delegate_task` — not the main agent role-playing. Each gets its own
conversation context, model call, and toolset.
**Core value**: Turn "one agent working alone" into "a team having a meeting."
## When to Use
- **Tech design review**: product, frontend, backend, architect debate approach
- **Competitive analysis**: product, marketing, design compare alternatives
- **Bug root cause analysis**: backend, ops, test triangulate the issue
- **Product requirements**: product, design, dev align on scope
- **Architecture decisions**: architect, backend, frontend, devops choose stack
## Prerequisites
Enable the `roundtable` toolset in the profile config:
```yaml
toolsets:
- roundtable
```
Or pass `enabled_toolsets: ["roundtable"]` when spawning an agent.
## Tools
| Tool | Purpose |
|------|---------|
| `roundtable_init` | Create a discussion with topic + participants (+ optional notifications config) |
| `roundtable_speak` | Record a participant's speech (auto-triggers notifications if configured) |
| `roundtable_read` | Read discussion history |
| `roundtable_status` | Check status + convergence metrics |
| `roundtable_summarize` | Get structured data for conclusion doc (use `compact=true` for smaller output) |
| `roundtable_end` | Conclude or cancel a discussion (triggers concluded notification) |
| `roundtable_list` | List all discussions |
| `roundtable_advance` | Manually advance to next round (use when auto-advance doesn't trigger) |
| `roundtable_notify` | Manually trigger a notification event for a discussion |
## Execution Model (Important)
Each participant is a **real, independent sub-agent**:
1. Coordinator calls `delegate_task(goal=..., toolsets=["roundtable"])` for each participant
2. The sub-agent runs its own model inference with its own context window
3. The sub-agent calls `roundtable_speak(participant="{profile}", ...)` to record its speech
4. The sub-agent returns a summary to the coordinator
5. Coordinator sends notification (optional), then delegates to next participant
**This means**: 4 participants × 4 rounds = 16 `delegate_task` calls = 16 independent
model invocations. Expect ~15-20 minutes for a full discussion. Each participant
takes 15-60 seconds per round depending on context size and model speed.
## Coordinator Flow
### Step 1: Create the Discussion
```
roundtable_init(
topic="Database selection: PostgreSQL vs MySQL vs TiDB",
context="Our e-commerce system needs high-concurrency read/write, 1TB+ data",
participants=[
{"profile": "bingge", "role": "Product Director", "perspective": "Focus on UX", "display_name": "Bing"},
{"profile": "mafei", "role": "Tech Lead", "perspective": "Focus on feasibility", "display_name": "Fei"},
{"profile": "xiaosu", "role": "Designer", "perspective": "Focus on data display", "display_name": "Su"},
],
created_by="coordinator", # optional: identifies the discussion creator
max_rounds=3,
speech_order="fixed",
notifications={
"enabled": True,
"channels": [
{"platform": "feishu", "chat_id": "oc_xxx"},
],
"events": ["round_start", "speech", "round_end", "concluded"],
}
)
→ returns {discussion_id, ...}
```
**Notifications** (optional): pushes real-time discussion updates to messaging
channels. See [Notifications](#notifications) section below.
### Step 2: Opening Statement (Round 0)
```
roundtable_speak(
discussion_id="rt_xxxxxxxx",
participant="coordinator",
content="Today we're discussing database selection..."
)
```
Optionally send opening notification:
```
send_message(target="feishu:oc_xxx", message="🔔 Roundtable started [rt_xxx]\n📋 Topic: ...\n👥 Participants: ...")
```
### Step 3: Multi-Round Discussion
For each round:
**3a. Coordinator summarizes previous round (optional but recommended):**
```
roundtable_speak(
discussion_id="rt_xxxxxxxx",
participant="coordinator",
content="Round N summary: key points, emerging consensus, open questions..."
)
```
**3b. Delegate to each participant SEQUENTIALLY (not parallel):**
```python
delegate_task(
goal="You are {display_name}, {role}. Share your viewpoint on this topic from a {role} perspective, then call roundtable_speak to record your speech.",
context="""You are participating in a roundtable discussion.
## Discussion Info
- Topic: {topic}
- Context: {context}
- Current Round: Round {N} / {max_rounds}
- Your Role: {role} ({display_name})
- Your Perspective: {perspective}
## Discussion History
{formatted_history}
## Your Task
From your role's perspective, share your viewpoint on this topic.
- Reference others' points if relevant
- Keep it 200-500 words
- Both pros AND cons, with concrete examples
After speaking, call roundtable_speak to record your statement:
roundtable_speak(discussion_id="{id}", participant="{profile}", content="your speech")""",
toolsets=["roundtable"]
)
# Wait for completion, then send notification, then delegate to participant 2
```
**3c. After each participant, send notification (if configured):**
```
send_message(target="feishu:oc_xxx", message="💬 Round {N} | {role} ({display_name}) spoke:\n{summary}")
```
**3d. After all participants in a round, send round_end notification:**
```
send_message(target="feishu:oc_xxx", message="✅ Round {N} complete\nConsensus: ...\nDisagreements: ...")
```
**Why sequential, not parallel**: Participants need to read each other's responses to build on them. Parallel delegation means everyone speaks into a void.
### Step 4: Check Convergence
After each round:
```
roundtable_status(discussion_id="rt_xxxxxxxx")
→ check convergence_score, consensus_points, disagreement_points
```
**Note**: Convergence tracking may not work reliably due to the round-tracking bug. The coordinator should manually assess convergence from the discussion content.
### Step 5: Generate Conclusion
```
summary = roundtable_summarize(discussion_id="rt_xxxxxxxx")
```
**⚠️ WARNING**: `roundtable_summarize` returns the ENTIRE discussion as raw JSON — potentially 100KB+. It does NOT generate a summary. The coordinator must:
1. Read the summary data (use `read_file` with offset/limit for the persisted output)
2. Write the conclusion document themselves based on the discussion content
3. Save to the `output_path` specified during init (or write manually via `write_file`)
**Important**: Write the conclusion doc BEFORE calling `roundtable_end` — the end call only accepts a brief text string, not a full document.
### Step 6: End Discussion
```
roundtable_end(
discussion_id="rt_xxxxxxxx",
conclusion="Brief text summary of the conclusion"
)
```
Send concluded notification:
```
send_message(target="feishu:oc_xxx", message="🏁 Discussion ended\nConclusion: ...")
```
## Participant Prompt Template
When delegating to a participant sub-agent, use this template:
```
You are participating in a roundtable discussion.
## Discussion Info
- Topic: {topic}
- Context: {context}
- Current Round: Round {current_round} / {max_rounds}
- Your Role: {role} ({display_name})
- Your Perspective: {perspective}
## Discussion History
{formatted_history}
## Your Task
From your role's perspective, share your viewpoint on this topic.
- You may reference or respond to other participants' statements
- Keep it concise and powerful, 200-500 words
- If you agree with a point, explicitly state your agreement
- If you disagree, explain why and propose alternatives
After speaking, call roundtable_speak to record your statement.
```
## Notifications
Push real-time discussion updates to messaging channels.
### Configuration
```python
roundtable_init(
topic="...",
participants=[...],
notifications={
"enabled": True,
"channels": [
{"platform": "feishu", "chat_id": "oc_xxx"},
],
"events": ["round_start", "speech", "round_end", "concluded"] # default: all
}
)
```
### Events
| Event | Trigger | Content |
|-------|---------|---------|
| `round_start` | First speech in a new round | Round number + previous round summary |
| `speech` | After each participant speaks | Speaker name/role + content (truncated to 200 chars) |
| `round_end` | All participants spoke in a round | Key points + convergence score |
| `concluded` | Discussion ends | Final conclusion + consensus/disagreement points |
### send_fn Requirement
The notification system requires a `send_fn(platform, chat_id, message)` callback
on `RoundtableCore`. The Hermes adapter (`adapters/hermes.py`) provides
`_hermes_send_fn` which calls `feishu-send.py` via subprocess.
**If send_fn is not wired**, notifications are silently disabled — the Notifier's
`enabled` returns `False`. Verify with: `core._send_fn is not None`.
**Manual fallback**: If send_fn is broken or unavailable, the coordinator can
manually push notifications using `send_message` after each speech:
```
send_message(target="feishu:oc_xxx", message="💬 Round {N} | Speaker: summary...")
```
### Timing
With automatic notifications (send_fn wired), each speech triggers a subprocess
call to feishu-send.py (~1-2s overhead). With manual `send_message`, overhead is
similar but requires explicit coordinator action after each speech.
### Verifying Notifications (Pitfall)
**Feishu API returns messages in chronological order by default** (oldest first).
When checking if roundtable notifications arrived in the group chat, you MUST use
`sort_type=ByCreateTimeDesc` in the API call — otherwise you see old messages and
conclude (incorrectly) that notifications didn't send.
**Wrong** — default sort returns oldest messages first:
```python
resp = requests.get('https://open.feishu.cn/open-apis/im/v1/messages',
params={'container_id': 'oc_xxx', 'page_size': 10})
```
**Correct** — sort descending to see latest messages:
```python
resp = requests.get('https://open.feishu.cn/open-apis/im/v1/messages',
params={'container_id': 'oc_xxx', 'page_size': 10, 'sort_type': 'ByCreateTimeDesc'})
```
**Debug pattern**: If notifications appear to not fire, add a wrapper `send_fn`
with logging to confirm the callback is actually invoked:
```python
def debug_send_fn(platform, chat_id, message):
print(f"[DEBUG SEND] platform={platform}, chat={chat_id}, msg_len={len(message)}")
original_send(platform, chat_id, message)
print(f"[DEBUG SEND] OK")
```
**Multiple roundtable.db files**: The system may have multiple databases:
- `~/.roundtable/roundtable.db` — main agent discussions
- `~/.hermes/roundtable.db` — hermes tool layer discussions
- `~/.hermes/profiles/{profile}/home/.roundtable/roundtable.db` — sub-agent discussions
Always verify against the correct DB when checking discussion state.
## Web Viewer (default ON)
`run_demo()` has `web: bool = True` by default (changed from `False`). This means demo discussions automatically start a web viewer at `http://localhost:8199`. The viewer uses PM2 to manage an Express subprocess, fcntl for file locking, and nanoid for token generation.
**Browser auto-open**: Starting a discussion with `web=True` should automatically open the browser via `subprocess.run(["open", web_url])` in the Hermes adapter's `_handle_init`. This is an **adapter-level side-effect**, not a core-level one — the core library returns the URL but leaves UX actions to the adapter. The generic adapter intentionally does NOT auto-open (headless environments). If you need to customize browser behavior (e.g., open in a specific browser), modify `_handle_init` in `adapters/hermes.py`.
**⚠️ Pitfall: Browser opens at END instead of START (2026-05-23)** — Boss reported the browser doesn't open when the discussion begins; it only opens after the discussion concludes (or manually). The `_handle_init` code has the `subprocess.run(["open", web_url])` call, but it may not fire reliably in all execution paths. **Diagnosis checklist**: (1) Verify `_handle_init` actually reaches the `subprocess.run` line (add logging) (2) Check if the `web_url` variable is correctly populated from `publisher.start()` return (3) Confirm the `open` command runs in the correct subprocess context (may need `shell=False` with list args on macOS). **See also**: Bug task `t_xxxxxxxx` for the specific fix.
**⚠️ Pitfall: WebViewer real-time updates broken on macOS (2026-05-23)** — Boss reported that the WebViewer doesn't show new speeches in real-time; the browser must be force-refreshed to see updates. **Root cause**: The Express server (`server.mjs`) uses `fs.watch(DISCUSSION_PATH, ...)` to detect `discussion.json` changes and broadcast via SSE. But Python's `WebPublisher` uses atomic write: `write .json.tmp → os.rename()`. On macOS, `fs.watch()` on a file loses tracking after `rename()` replaces the inode — the watcher stays on the old inode, never sees the new file's changes. **Fix**: Change `fs.watch` to watch the **directory** instead of the file, then filter by filename:
```javascript
// Wrong — breaks on atomic rename:
watch(DISCUSSION_PATH, () => { ... });
// Correct — watches directory, catches rename:
const dir = require("path").dirname(DISCUSSION_PATH);
const filename = require("path").basename(DISCUSSION_PATH);
watch(dir, (eventType, changedFilename) => {
if (changedFilename !== filename) return;
// ... debounce + broadcast logic unchanged
});
```
**Alternative**: Add server-side polling fallback (`setInterval` + mtime check) as defense-in-depth. **See also**: Bug task `t_xxxxxxxx` for the specific fix.
**⚠️ Direct Core API does NOT auto-open browser**: When creating discussions via Direct Core API (`core.create_discussion()`) instead of the `roundtable_init` tool, the adapter's `_handle_init` is NOT called, so:
1. The browser does NOT auto-open
2. The WebViewer URL is NOT printed to the user
3. The coordinator MUST manually extract `web_url` from the response and: (a) share it with the user, and (b) run `open "{web_url}"` to open the browser.
**Pattern**: After `core.create_discussion()` returns, parse `web_url` from the JSON response, then:
```bash
# Share URL with user (via send_message or inline)
# Then open browser
open "http://0.0.0.0:{port}/r/{token}"
```
## Install roundtable in Hermes venv (required for agent to import it):
```bash
# hermes venv may lack pip — bootstrap it first
~/.hermes/hermes-agent/venv/bin/python3 -m ensurepip
~/.hermes/hermes-agent/venv/bin/python3 -m pip install /path/to/roundtable
```
Note: system Python 3.9 on macOS is too old (package requires >=3.10). Always install into the hermes venv (Python 3.11+).
**Alternative: uv venv (when hermes venv is missing or broken)**:
```bash
# Create a dedicated venv with Python 3.12 via uv
uv venv /tmp/roundtable-venv --python 3.12
uv pip install -e /path/to/roundtable --python /tmp/roundtable-venv/bin/python3
# Then use this venv for all Direct Core API calls:
/tmp/roundtable-venv/bin/python3 << 'PYEOF'
import json, sys
sys.path.insert(0, "/Users/parsifal/.hermes/hermes-agent")
from tools.roundtable_tools import _get_core
core = _get_core()
# ...
PYEOF
```
This pattern is useful when:
- System Python is 3.9 (macOS default) and roundtable requires >=3.10
- The hermes venv doesn't exist or lacks pip
- You need a clean, isolated environment for roundtable
## Convergence Detection
Each round is evaluated for convergence:
| Metric | Formula | Meaning |
|--------|---------|---------|
| Consensus | Points multiple participants agree on | Alignment |
| Disagreement | Points participants disagree on | Conflict |
| New Point | New topics raised this round | Scope expansion |
| Score | consensus / (consensus + disagreement) | Overall alignment |
**Termination conditions:**
- Convergence score > 0.8 → high consensus, wrap up
- Max rounds reached → prevent infinite discussion
- Coordinator manually ends → emergency stop
- All participants vote to end → democratic close
## Conclusion Document Format
The format below works for general discussions. For **product/design/dev discussions aimed at producing a buildable specification**, use the decision-oriented format instead — see `src/skills/references/web-viewer-discussion-example.md` for the full pattern (MVP scope, tech architecture, acceptance criteria, risk assessment, design deliverables, action items).
```markdown
# Roundtable Conclusion: [Topic]
## Summary
- Participants: Product(Bing), Design(Su), Dev(Fei)
- Rounds: 3
- Date: 2026-05-20
## Consensus Points
1. [Point 1]
2. [Point 2]
## Disagreement Points
1. [Point 1] - Various perspectives
## Action Items
1. [ ] [Action 1] - Owner: xxx
2. [ ] [Action 2] - Owner: xxx
## Detailed Transcript
### Round 1
- **Product(Bing)**: ...
- **Design(Su)**: ...
- **Dev(Fei)**: ...
### Round 2
...
```
## Data Storage
- **Database**: `~/.hermes/roundtable.db` (independent from kanban.db)
- **Conclusion docs**: Configurable via `output_path`, defaults to project docs dir
- **ID format**: `rt_` + 8 hex chars (e.g., `rt_a1b2c3d4`)
## Integration with Kanban
Discussions can be linked to kanban tasks:
```
# After conclusion, add as task comment
kanban_comment(task_id="t_xxx", body="Roundtable conclusion: {conclusion_path}")
```
## Direct Core API Fallback (Last Resort Only)
> **⚠️ IMPORTANT**: The standard flow is ALWAYS `roundtable_init` + `delegate_task(toolsets=["roundtable"])`. Do NOT default to Direct Core API. If sub-agents fail to call `roundtable_speak`, treat it as a **bug to investigate and fix** — not a workaround to codify. The user explicitly rejected the workaround-first mindset: "不是本质问题吗?一直workaround?"
>
> **Before using Direct Core API, exhaust these options first:**
> 1. Check if the sub-agent's `enabled_toolsets` actually includes `"roundtable"`
> 2. Check if the sub-agent's profile has the `roundtable` toolset registered
> 3. Try with a different profile to isolate the issue
> 4. Report the bug as a kanban task for investigation (bug tracking task: `t_xxxxxxxx`)
>
> **Only use Direct Core API when**: (a) the bug is confirmed and (b) you need the discussion to proceed NOW while the fix is in progress.
**Known Issue**: Sub-agents spawned via `delegate_task` with `toolsets=["roundtable"]` sometimes fail to invoke `roundtable_speak` — the sub-agent outputs speech text but `tool_trace` is empty. This is a **bug**, not a design limitation. Root cause is under investigation.
**Direct Core API (emergency bypass only)**: Bypass the Hermes tool layer and call `RoundtableCore` directly:
```bash
~/.hermes/hermes-agent/venv/bin/python3 << 'PYEOF'
import json, sys
sys.path.insert(0, "/Users/parsifal/.hermes/hermes-agent")
from tools.roundtable_tools import _get_core
core = _get_core()
# Record a speech
result = core.speak(
discussion_id="rt_xxxxxxxx",
participant="bingge", # must match participants list exactly
content='Speech content here' # use single quotes to avoid heredoc issues
)
print(json.dumps(result, ensure_ascii=False))
# Read discussion history
history = core.read(discussion_id="rt_xxxxxxxx")
print(json.dumps(history, ensure_ascii=False, indent=2))
# End discussion
core.end_discussion(discussion_id="rt_xxxxxxxx", conclusion="Brief conclusion text")
PYEOF
```
**Key gotchas**:
- **NEVER import `RoundtableCore` directly** — always use `_get_core()` from `tools.roundtable_tools`. Direct import bypasses `send_fn` wiring, so notifications silently fail. Boss explicitly corrected this (2026-05-23): "你这压根不是用我们开发的圆桌技能做啊".
- Use single quotes for content strings containing double quotes (heredoc escaping)
- `participant` must exactly match the profile name in participants list
- `coordinator` is always allowed (bypasses participant check)
- Each `core.speak()` returns `round_complete` and `next_speaker` — use to track progress
- `core._send_fn` must be non-None for notifications to fire (wired by `_hermes_send_fn`)
- **Cross-process singleton**: `_get_core()` returns a singleton, BUT each Python invocation is a fresh process, so the singleton only lives for that one script execution. The `_publishers` dict (WebPublisher) is in-memory and lost between invocations. This is handled by the `_update_web_discussion_json` fallback — see "Cross-Process WebPublisher Data Sync" section.
**Hybrid Workflow (temporary measure only)**: If `roundtable_speak` is confirmed broken in sub-agents, use `delegate_task` for reasoning and Direct Core API for recording — but treat this as a **temporary workaround** while the bug is being fixed, not a permanent pattern.
```python
# Step 1: Delegate to sub-agent for thought generation
result = delegate_task(
goal="You are the Product Director. Share your viewpoint on '{topic}' from a product perspective...",
context="...",
toolsets=["roundtable"] # sub-agent will fail to call roundtable_speak — that's expected
)
# Step 2: Extract speech text from sub-agent's summary
speech_text = extract_from_summary(result) # parse the sub-agent's output
# Step 3: Record via Direct Core API (100% reliable)
terminal(f"""
~/.hermes/hermes-agent/venv/bin/python3 << 'PYEOF'
import json, sys
sys.path.insert(0, "/Users/parsifal/.hermes/hermes-agent")
from tools.roundtable_tools import _get_core
core = _get_core()
result = core.speak(discussion_id="{discussion_id}", participant="{profile}", content='''{speech_text}''')
print(json.dumps(result, ensure_ascii=False))
PYEOF
""")
```
**Key insight**: The sub-agent's reasoning is valuable — don't waste it. Just don't rely on it to persist the data.
**When to use Direct Core API**: Any time reliability matters — demos, verification meetings, production discussions. The direct API is faster (no sub-agent overhead) and 100% reliable.
24. **Quick verification meetings** — When Boss says "组织一次会议看效果", use the lightweight pattern: coordinator speaks for all participants directly via `roundtable_speak` (no `delegate_task`). Runs in ~2min vs ~15-20min. Always include notifications config even for demos — the goal is to verify the full pipeline including group chat sync. See `src/skills/references/quick-verification-example.md` for a working example.
25. **delegate_task sub-agents can fail to call roundtable_speak** — Observed in 2026-05-23 session (6/6 failures). Sub-agents produce speech text in their response but `tool_trace` is empty. **DO NOT assume this is permanent** — it's a bug under investigation (`t_xxxxxxxx`). Before falling back to Direct Core API, check: (1) sub-agent's `enabled_toolsets` includes `"roundtable"`, (2) the profile has roundtable tools registered, (3) try a different profile. If confirmed broken, use Direct Core API as temporary bypass and file/update the bug task.
26. **Direct Core API is an emergency bypass, NOT the default** — Use `roundtable_init` + `delegate_task` as the primary flow. Direct Core API (`core.speak()`) exists for when the standard flow is confirmed broken AND you need the discussion to proceed now. Always file a bug task when you're forced to use it.
27. **`web` and `web_port` params on roundtable_init** — The core supports `web=True` to auto-start a WebPublisher HTTP server. The Hermes tool schema now includes these params. Default `web=True` in `_handle_init` so discussions always open WebViewer. Port auto-increments if 8199 is busy (check `web_url` in init response for actual port).
28. **Adapter handles UX side-effects, core returns data** — When adding features that interact with the user's environment (browser, notifications, file watchers), implement them at the **adapter level** (`adapters/hermes.py`), not in `core.py`. The core library should be stateless and headless — it returns data (URLs, IDs, status) and the adapter decides what to do with it. Example: browser auto-open lives in `_handle_init` (adapter), not in `create_discussion` (core). The generic adapter may choose different UX behavior or none at all.
> **Notifications implementation detail**: See `src/skills/references/notifications-implementation.md` for architecture, send_fn wiring, and execution flow diagrams. See `src/skills/references/notification-debugging.md` for verification pitfalls and debugging checklist.
1. **At least 2 participants required** — A discussion needs multiple viewpoints
2. **Participant must be registered** — Only profiles listed in `participants` can speak. **Exception**: `participant="coordinator"` is always allowed (bypasses the participant check). This was a bug fix — previously, `roundtable_speak(participant="coordinator")` would fail with "not an active member". Coordinator's speech is recorded in round 0 and does NOT affect round advancement logic.
3. **Round 0 is opening** — Coordinator speaks first, then round 1 begins
4. **Auto-conclude on max_rounds** — Discussion ends automatically when max rounds exceeded
5. **Independent database** — roundtable.db is separate from kanban.db; don't mix paths
6. **No LLM in summarize** — `roundtable_summarize` returns raw JSON data (potentially 100KB+), NOT a summary. The coordinator must manually write the conclusion document. Use `read_file` with offset/limit to process the persisted output.
7. **connect() expects Path, not str** — Use `connect(Path("~/.hermes/roundtable.db"))`, not `connect("/Users/...")`
8. **delegate_task needs roundtable toolset** — Sub-agents spawned via `delegate_task` must have `enabled_toolsets=['roundtable']` to use roundtable tools
9. **Round tracking is buggy** — All speeches may end up as Round 0 regardless of actual round. The `round` field in speak responses may not reflect reality. Workaround: track rounds manually in the coordinator's logic.
10. **No `roundtable_advance` tool** — There is no tool to explicitly advance rounds. Rounds are supposed to auto-advance when all participants have spoken, but due to the round-tracking bug this may not work. The coordinator should track round progress manually.
11. **Sequential delegation, not parallel** — Delegate to participants one at a time so they can read each other's responses. Parallel delegation means everyone speaks without context.
12. **Coordinator summarizes between rounds** — After each round of participant speeches, the coordinator should call `roundtable_speak(participant="coordinator")` to summarize before delegating the next round. This gives participants context for their next response.
13. **summarize output is persisted as file** — When `roundtable_summarize` returns >8K chars, the full output is saved to a temp file. Use `read_file(path, offset, limit)` to process it in chunks.
14. **Hermes adapter send_fn must be wired** — The `adapters/hermes.py` creates `RoundtableCore()` without passing a `send_fn`, so notifications configured in `roundtable_init` will silently not send. Fix: implement `_hermes_send_fn(platform, chat_id, message)` that calls `feishu-send.py`, then pass it to `RoundtableCore(send_fn=_hermes_send_fn)`. Default profile should be `"default"` (not hardcoded to any specific profile). Without this fix, notifications are metadata only.
15. **Participants are real sub-agents, not role-play** — Each participant is a separate `delegate_task()` call that spawns an independent sub-agent with its own context window, model inference, and tool access. This is why a 4-round, 4-participant discussion takes 15-20 minutes (16+ independent model calls). The coordinator drives rounds sequentially so participants can read each other's responses.
16. **Generic adapter lacks notifications** — `adapters/generic.py` does not support `notifications` config or `send_fn`. Non-Hermes users must manually handle event dispatch. If releasing as standalone library, the generic adapter needs `send_fn` parameter support and `advance()` + `notify()` methods.
14. **send_fn must be wired in adapter** — The `Notifier` requires a `send_fn` callback to actually send messages. In `adapters/hermes.py`, `_get_core()` must pass `send_fn=_hermes_send_fn` to `RoundtableCore()`. Without this, notifications config is silently ignored (Notifier.enabled returns False). If notifications aren't firing, check that `_hermes_send_fn` is implemented and passed to the core. The send_fn uses `subprocess.run(["python3", script, profile, chat_id, message])` to call `feishu-send.py`.
15. **HERMES_PROFILE env var** — The send_fn reads `os.environ.get("HERMES_PROFILE", "default")` to determine which profile's Bot sends the notification. Default is `"default"` (coordinator). This is set automatically by the Hermes runtime — don't hardcode it.
16. **Real sub-agents, not role-playing** — Each participant in a roundtable discussion is a separate `delegate_task` call spawning an independent sub-agent with its own context window and model call. This is why 4 participants × 4 rounds = 16 delegate_task calls and ~15-20 minutes of runtime. If it were just the main agent role-playing, it would take seconds.
17. **Notifications don't block** — All notification failures are caught and logged. A broken send_fn never breaks the discussion flow. This is by design.
18. **Sub-agent context window** — Each participant sub-agent only sees what the coordinator passes in `context`. If you omit discussion history, the participant speaks "into a void". Always include `formatted_history` from `roundtable_read`.
19. **Write conclusion doc BEFORE roundtable_end** — `roundtable_end` only accepts a brief `conclusion` text string (1-2 sentences). The full conclusion document (with action items, tables, detailed transcript) must be written as a separate file via `write_file`. Do this BEFORE calling `roundtable_end` so you can still use `roundtable_summarize` if needed.
20. **Round numbering mismatch** — The `roundtable_summarize` output labels speeches as "Round 0", "Round 1", "Round 2" based on the DB's internal round counter. The coordinator's mental model (Round 1 = first participant round) may differ from the DB's (Round 0 = opening, Round 1 = first participant round). Always check the actual round field in the data rather than assuming.
21. **History reading cost grows linearly** — `roundtable_read` returns ALL speeches. In Round 3 of a 3-participant discussion, the first participant's `delegate_task` took 165s because the context included 9 full speeches. For longer discussions (4+ rounds, 4+ participants), consider summarizing previous rounds in the coordinator's `roundtable_speak` between rounds to keep context manageable.
22. **No convergence metrics in practice** — `roundtable_summarize` returns `final_convergence_score: null` and empty `consensus_points`/`disagreement_points`. The convergence detection is not implemented in the core engine. The coordinator MUST manually assess convergence from discussion content — don't rely on the status/summarize tools for this.
23. **Coordinator MUST pass notifications config** — Even though `send_fn` is wired in the adapter, `notifications` is an OPTIONAL parameter on `roundtable_init`. If you don't pass it, no events fire and no messages reach the group chat — silently. This is the #1 cause of "why didn't my discussion sync to the group?" Always include notifications when the discussion should be visible to the team. For OPC team, the standard config is: `notifications={"enabled": True, "channels": [{"platform": "feishu", "chat_id": "oc_your_company_group_id"}], "events": ["round_start", "speech", "round_end", "concluded"]}`. Without this explicit config, discussions run in "silent mode" — useful for testing, but almost never what you want in production.
## Cross-Process WebPublisher Data Sync (Critical Pitfall)
**Problem**: Hermes tool calls (`roundtable_init`, `roundtable_speak`, `roundtable_end`) each run in a **separate Python process**. The `_publishers` dict in `RoundtableCore` is in-memory — it's populated during `create_discussion()` in process A, but `speak()` runs in process B where `_publishers` is empty. Result: `publisher.on_speech()` is never called, `discussion.json` stays at `"speeches": []`, and the WebViewer shows nothing.
**Fix** (applied 2026-05-23): Two cross-process fallback methods in `core.py`:
- `_update_web_discussion_json()` — called in `speak()` when publisher not in memory; reads/writes `discussion.json` directly with fcntl locking
- `_conclude_web_discussion()` — called in `end_discussion()` when publisher not in memory; sets status="concluded" and writes conclusion
These methods find the JSON file at `/tmp/roundtable_web/{discussion_id}/discussion.json` and update it with the same atomic write pattern (flock → write .tmp → fsync → rename) that WebPublisher uses.
**Also fixed**: `publisher.conclude()` was called without the conclusion string argument — fixed to pass `disc_after.conclusion or ""`.
**Verification**: Tested with fully independent processes (create in process 1, 6 speeches each in separate processes, end in process 8). All 6 speeches appeared in WebViewer, conclusion displayed correctly.
**Impact**: Without this fix, the WebViewer is essentially non-functional in real Hermes usage since every tool call is a fresh process.
## Test Results
See `src/skills/references/test-results-2026-05-20.md` for the first functional test results, including bugs found and product acceptance report.
## Open-Source Release
See `src/skills/references/open-source-readiness.md` for the pre-release checklist (LICENSE, cleanup, adapter gaps, test isolation).
## Working Examples
- `src/skills/references/opc-experience-discussion-example.md` — 4-round, 4-participant discussion with timing data and workflow
- `src/skills/references/notifications-example.md` — roundtable with real-time push notifications to Feishu
- `src/skills/references/release-planning-discussion.md` — 3-round product/design/dev discussion for open-source release planning
- `src/skills/references/ai-relay-open-source-discussion.md` — 3-round discussion with hybrid workflow (delegate_task + Direct Core API), notifications, and conclusion doc → 5/29 release plan
- `src/skills/references/web-viewer-discussion-example.md` — Decision-oriented conclusion doc pattern: MVP scope, tech architecture, acceptance criteria, risk assessment, design deliverables. Use this format when the discussion goal is to produce a buildable specification.
- `src/skills/references/post-discussion-kanban-dispatch.md` — After discussion concludes, create kanban tasks grouped by owner, subscribe notifications, and dispatch to team via Feishu groups.
## Open-Source Release
See `src/skills/references/open-source-readiness-checklist.md` for the pre-release audit: missing LICENSE, Hermes-specific files to separate, build-backend fix, .gitignore, internal docs to remove, generic adapter gaps, and target package structure.
# Test line
don't have the plugin yet? install it then click "run inline in claude" again.