Based on actual Flawd deployment experience:
- Proper fly.toml configuration with all required settings
- Step-by-step guide following exe.dev doc format
- Troubleshooting section with common issues and fixes
- Config file creation via SSH
- Cost estimates
In containers, PIDs can be recycled quickly after restarts. When a container
restarts, a different process might get the same PID as the previous gateway,
causing the lock check to incorrectly think the old gateway is still running.
This fix adds isGatewayProcess() which verifies on Linux that the PID actually
belongs to a clawdbot gateway by checking /proc/PID/cmdline. If the cmdline
doesn't contain 'clawdbot' or 'gateway', we assume the lock is stale.
Fixes gateway boot-loop in Docker/Fly.io deployments.
- Add fly.toml configuration for Fly.io deployment
- Add docs/platforms/fly.md with deployment guide
- Uses London (lhr) region by default
- Includes persistent volume for data storage
- Add latency metrics to summarizeText and textToSpeech functions
- Add /tts_status command showing config and last attempt result
- Add /tts_summary command for feature flag control
- Fix atomic write to clean up temp file on rename failure
- Add timer.unref() to prevent blocking process shutdown
- Add unit tests for validation functions (13 tests)
- Update README with new commands and features
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add /tts_limit command to configure max text length (default 1500)
- Auto-summarize long texts with gpt-4o-mini before TTS conversion
- Add truncation safeguard if summary exceeds hard limit
- Validate targetLength parameter (100-10000)
- Use conservative max_tokens for multilingual text
- Add prompt injection defense with XML delimiters
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Integrate message_sending hook into Telegram delivery path
- Send text first, then audio as voice message after
- Add /tts_provider command to switch between OpenAI and ElevenLabs
- Implement automatic fallback when primary provider fails
- Use gpt-4o-mini-tts as default OpenAI model
- Add hook integration to route-reply.ts for other channels
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove sag CLI dependency
- Add direct ElevenLabs API integration via fetch
- Add OpenAI TTS as alternative provider
- Support multi-provider configuration
- Add tts.providers RPC method
- Update config schema with OpenAI options
- Bump version to 0.2.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add a new extension that provides automatic text-to-speech for chat
responses using ElevenLabs API.
Features:
- `speak` tool for converting text to voice messages
- RPC methods: tts.status, tts.enable, tts.disable, tts.convert
- User preferences file for persistent TTS state
- Configurable voice ID, model, and max text length
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Address reviewer feedback: slash commands now use the same
hasExplicitConfig check as regular messages, so unlisted
channels are allowed under groupPolicy: "open" for both
message handling and slash commands.
## Summary
Fix Slack `groupPolicy: "open"` to allow unlisted channels even when `channels.slack.channels` contains custom entries.
## Problem
When `groupPolicy` is set to `"open"`, the bot should respond in **any channel** it's invited to. However, if `channels.slack.channels` contains *any* entries—even just one channel with a custom system prompt—the open policy is ignored. Only explicitly listed channels receive responses; all others get an ephemeral "This channel is not allowed" error.
### Example config
```json
{
"channels": {
"slack": {
"groupPolicy": "open",
"channels": {
"C0123456789": { "systemPrompt": "Custom prompt for this channel" }
}
}
}
}
```
With this config, the bot only responds in `C0123456789`. Messages in any other channel are blocked—even though the policy is `"open"`.
## Root Cause
In `src/slack/monitor/context.ts`, `isChannelAllowed()` has two sequential checks:
1. `isSlackChannelAllowedByPolicy()` — correctly returns `true` for open policy
2. A secondary `!channelAllowed` check — was blocking channels when `resolveSlackChannelConfig()` returned `{ allowed: false }` for unlisted channels
The second check conflated "channel not in config" with "channel explicitly denied."
## Fix
Use `matchSource` to distinguish explicit denial from absence of config:
```ts
const hasExplicitConfig = Boolean(channelConfig?.matchSource);
if (!channelAllowed && (params.groupPolicy !== "open" || hasExplicitConfig)) {
return false;
}
```
When `matchSource` is undefined, the channel has no explicit config entry and should be allowed under open policy.
## Behavior After Fix
| Scenario | Result |
|----------|--------|
| `groupPolicy: "open"`, channel unlisted | ✅ Allowed |
| `groupPolicy: "open"`, channel explicitly denied (`allow: false`) | ❌ Blocked |
| `groupPolicy: "open"`, channel with custom config | ✅ Allowed |
| `groupPolicy: "allowlist"`, channel unlisted | ❌ Blocked |
## Test Plan
- [x] Open policy + unlisted channel → allowed
- [x] Open policy + explicitly denied channel → blocked
- [x] Allowlist policy + unlisted channel → blocked
- [x] Allowlist policy + listed channel → allowed
Added a dedicated Anthropic payload logger that writes exact request
JSON (as sent) plus per‑run usage stats (input/output/cache read/write)
to a
standalone JSONL file, gated by an env flag.
Changes
- New logger: src/agents/anthropic-payload-log.ts (writes
logs/anthropic-payload.jsonl under the state dir, optional override via
env).
- Hooked into embedded runs to wrap the stream function and record
usage: src/agents/pi-embedded-runner/run/attempt.ts.
How to enable
- CLAWDBOT_ANTHROPIC_PAYLOAD_LOG=1
- Optional:
CLAWDBOT_ANTHROPIC_PAYLOAD_LOG_FILE=/path/to/anthropic-payload.jsonl
What you’ll get (JSONL)
- stage: "request" with payload (exact Anthropic params) +
payloadDigest
- stage: "usage" with usage
(input/output/cacheRead/cacheWrite/totalTokens/etc.)
Notes
- Usage is taken from the last assistant message in the run; if the
run fails before usage is present, you’ll only see an error field.
Files touched
- src/agents/anthropic-payload-log.ts
- src/agents/pi-embedded-runner/run/attempt.ts
Tests not run.
- Add registry lock during command execution to prevent race conditions
- Add input sanitization for command arguments (defense in depth)
- Validate handler is a function during registration
- Remove redundant case-insensitive regex flag
- Add success logging for command execution
- Simplify handler return type (always returns result now)
- Remove dead code branch in commands-plugin.ts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add clearPluginCommands() call in loadClawdbotPlugins() to ensure
previously registered commands are cleaned up before reloading plugins.
This prevents command conflicts during hot-reload scenarios.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Blockers fixed:
- Fix documentation: requireAuth defaults to true (not false)
- Add command name validation (must start with letter, alphanumeric only)
- Add reserved commands list to prevent shadowing built-in commands
- Emit diagnostic errors for invalid/duplicate command registration
Other improvements:
- Return user-friendly message for unauthorized commands (instead of silence)
- Sanitize error messages to avoid leaking internal details
- Document acceptsArgs behavior when arguments are provided
- Add notes about reserved commands and validation rules to docs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This adds a new `api.registerCommand()` method to the plugin API, allowing
plugins to register slash commands that execute without invoking the AI agent.
Features:
- Plugin commands are processed before built-in commands and the agent
- Commands can optionally require authorization
- Commands can accept arguments
- Async handlers are supported
Use case: plugins can implement toggle commands (like /tts_on, /tts_off)
that respond immediately without consuming LLM API calls.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: skip heartbeat API calls when HEARTBEAT.md is effectively empty
- Added isHeartbeatContentEffectivelyEmpty() to detect files with only headers/comments
- Modified runHeartbeatOnce() to check HEARTBEAT.md content before polling the LLM
- Returns early with 'empty-heartbeat-file' reason when no actionable tasks exist
- Preserves existing behavior when file is missing (lets LLM decide)
- Added comprehensive test coverage for empty file detection
- Saves API calls/costs when heartbeat file has no meaningful content
* chore: update HEARTBEAT.md template to be effectively empty by default
Changed instruction text to comment format so new workspaces benefit from
heartbeat optimization immediately. Users still get clear guidance on usage.
* fix: only treat markdown headers (# followed by space) as comments, not #TODO etc
* refactor: simplify regex per code review suggestion
* docs: clarify heartbeat empty file behavior (#1535) (thanks @JustYannicc)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>