feat(signal): default Signal read receipts to true (#576)

Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
Cameron
2026-03-12 09:10:38 -07:00
committed by GitHub
parent a6b1a43ec5
commit 1d636d6fa9
8 changed files with 76 additions and 10 deletions

View File

@@ -92,6 +92,7 @@ channels:
signal:
enabled: true
phone: "+1234567890"
readReceipts: true
selfChat: true
dmPolicy: pairing
@@ -239,6 +240,7 @@ agents:
channels:
signal:
phone: "+1234567890"
readReceipts: true
selfChat: true
whatsapp:
enabled: true
@@ -521,6 +523,7 @@ For dedicated bot numbers (`selfChat: false`), onboarding defaults to **allowlis
| Option | Type | Description |
|--------|------|-------------|
| `phone` | string | Phone number with + prefix |
| `readReceipts` | boolean | Send read receipts for incoming messages (default: `true`) |
| `selfChat` | boolean | `true` = only "Note to Self" works |
## Features Configuration
@@ -1052,6 +1055,7 @@ Reference:
| `WHATSAPP_ENABLED` | `channels.whatsapp.enabled` |
| `WHATSAPP_SELF_CHAT_MODE` | `channels.whatsapp.selfChat` |
| `SIGNAL_PHONE_NUMBER` | `channels.signal.phone` |
| `SIGNAL_READ_RECEIPTS` | `channels.signal.readReceipts` |
| `OPENAI_API_KEY` | `transcription.apiKey` |
| `GMAIL_ACCOUNT` | `polling.gmail.account` (comma-separated list allowed) |
| `POLLING_INTERVAL_MS` | `polling.intervalMs` |

View File

@@ -79,6 +79,9 @@ SIGNAL_PHONE_NUMBER=+17075204676
# Optional: Self-chat mode for "Note to Self" (default: true)
# SIGNAL_SELF_CHAT_MODE=true
# Optional: Send read receipts for incoming messages (default: true)
# SIGNAL_READ_RECEIPTS=true
```
**Note:** For personal numbers (`selfChatMode: true`), `dmPolicy` is ignored - only you can message via "Note to Self". For dedicated bot numbers, onboarding defaults to `allowlist`.
@@ -97,6 +100,7 @@ The daemon runs on port 8090 by default to avoid conflicts with other services.
- **Direct Messages** - Receive and respond to DMs
- **Note to Self** - Use Signal's "Note to Self" feature to message yourself (selfChatMode)
- **Allowlist** - For dedicated numbers, only pre-approved phone numbers can message
- **Read Receipts** - Enabled by default (disable with `SIGNAL_READ_RECEIPTS=false`)
## Troubleshooting

View File

@@ -94,6 +94,7 @@ const SHARED_CHANNEL_BUILDERS: SharedChannelBuilder[] = [
cliPath: signal.cliPath || process.env.SIGNAL_CLI_PATH || 'signal-cli',
httpHost: signal.httpHost || process.env.SIGNAL_HTTP_HOST || '127.0.0.1',
httpPort: signal.httpPort || parseInt(process.env.SIGNAL_HTTP_PORT || '8090', 10),
readReceipts: signal.readReceipts ?? (process.env.SIGNAL_READ_RECEIPTS !== 'false'),
dmPolicy: signal.dmPolicy || 'pairing',
allowedUsers: nonEmpty(signal.allowedUsers),
selfChatMode,

View File

@@ -1,6 +1,13 @@
import { describe, it, expect, vi } from 'vitest';
import { SignalAdapter } from './signal.js';
type SignalAdapterWithInternals = {
config: {
readReceipts?: boolean;
};
buildDaemonArgs: () => string[];
};
describe('SignalAdapter sendFile', () => {
function createAdapter(phone = '+15555555555') {
return new SignalAdapter({ phoneNumber: phone });
@@ -87,3 +94,22 @@ describe('SignalAdapter sendFile', () => {
expect(result.messageId).toBe('unknown');
});
});
describe('SignalAdapter read receipts', () => {
it('defaults readReceipts to true', () => {
const adapter = new SignalAdapter({ phoneNumber: '+15555555555' });
const internal = adapter as unknown as SignalAdapterWithInternals;
expect(internal.config.readReceipts).toBe(true);
expect(internal.buildDaemonArgs()).toContain('--send-read-receipts');
});
it('omits daemon read receipts flag when disabled', () => {
const adapter = new SignalAdapter({
phoneNumber: '+15555555555',
readReceipts: false,
});
const internal = adapter as unknown as SignalAdapterWithInternals;
expect(internal.config.readReceipts).toBe(false);
expect(internal.buildDaemonArgs()).not.toContain('--send-read-receipts');
});
});

View File

@@ -35,6 +35,7 @@ export interface SignalConfig {
httpHost?: string; // Daemon HTTP host (default: "127.0.0.1")
httpPort?: number; // Daemon HTTP port (default: 8090)
startupTimeoutMs?: number; // Max time to wait for daemon startup (default: 30000)
readReceipts?: boolean; // Send read receipts for incoming messages (default: true)
// Security
dmPolicy?: DmPolicy; // 'pairing' (default), 'allowlist', or 'open'
allowedUsers?: string[]; // Phone numbers (config allowlist)
@@ -171,6 +172,7 @@ export class SignalAdapter implements ChannelAdapter {
this.config = {
...config,
dmPolicy: config.dmPolicy || 'pairing',
readReceipts: config.readReceipts !== false, // Default true
selfChatMode: config.selfChatMode !== false, // Default true
};
const host = config.httpHost || '127.0.0.1';
@@ -394,21 +396,28 @@ This code expires in 1 hour.`;
}
// --- Private methods ---
private async startDaemon(): Promise<void> {
const cliPath = this.config.cliPath || 'signal-cli';
const host = this.config.httpHost || '127.0.0.1';
const port = this.config.httpPort || 8090;
private buildDaemonArgs(): string[] {
const args: string[] = [];
if (this.config.phoneNumber) {
args.push('-a', this.config.phoneNumber);
}
args.push('daemon');
args.push('--http', `${host}:${port}`);
args.push('--http', `${this.config.httpHost || '127.0.0.1'}:${this.config.httpPort || 8090}`);
args.push('--no-receive-stdout');
if (this.config.readReceipts !== false) {
args.push('--send-read-receipts');
}
return args;
}
private async startDaemon(): Promise<void> {
const cliPath = this.config.cliPath || 'signal-cli';
const args = this.buildDaemonArgs();
log.info(`Spawning: ${cliPath} ${args.join(' ')}`);

View File

@@ -379,6 +379,10 @@ export function configToEnv(config: LettaBotConfig): Record<string, string> {
}
if (config.channels.signal?.phone) {
env.SIGNAL_PHONE_NUMBER = config.channels.signal.phone;
// Signal readReceipts defaults to true, so only set env if explicitly false
if (config.channels.signal.readReceipts === false) {
env.SIGNAL_READ_RECEIPTS = 'false';
}
// Signal selfChat defaults to true, so only set env if explicitly false
if (config.channels.signal.selfChat === false) {
env.SIGNAL_SELF_CHAT_MODE = 'false';

View File

@@ -27,7 +27,7 @@ describe('normalizeAgents', () => {
'TELEGRAM_BOT_TOKEN', 'TELEGRAM_DM_POLICY', 'TELEGRAM_ALLOWED_USERS',
'SLACK_BOT_TOKEN', 'SLACK_APP_TOKEN', 'SLACK_DM_POLICY', 'SLACK_ALLOWED_USERS',
'WHATSAPP_ENABLED', 'WHATSAPP_SELF_CHAT_MODE', 'WHATSAPP_DM_POLICY', 'WHATSAPP_ALLOWED_USERS',
'SIGNAL_PHONE_NUMBER', 'SIGNAL_SELF_CHAT_MODE', 'SIGNAL_DM_POLICY', 'SIGNAL_ALLOWED_USERS',
'SIGNAL_PHONE_NUMBER', 'SIGNAL_SELF_CHAT_MODE', 'SIGNAL_READ_RECEIPTS', 'SIGNAL_DM_POLICY', 'SIGNAL_ALLOWED_USERS',
'DISCORD_BOT_TOKEN', 'DISCORD_DM_POLICY', 'DISCORD_ALLOWED_USERS',
'BLUESKY_WANTED_DIDS', 'BLUESKY_WANTED_COLLECTIONS', 'BLUESKY_JETSTREAM_URL', 'BLUESKY_CURSOR',
'BLUESKY_HANDLE', 'BLUESKY_APP_PASSWORD', 'BLUESKY_SERVICE_URL', 'BLUESKY_APPVIEW_URL',
@@ -594,9 +594,25 @@ describe('normalizeAgents', () => {
expect(agents[0].channels.slack?.appToken).toBe('slack-app');
expect(agents[0].channels.whatsapp?.enabled).toBe(true);
expect(agents[0].channels.signal?.phone).toBe('+1234567890');
expect(agents[0].channels.signal?.readReceipts).toBe(true);
expect(agents[0].channels.discord?.token).toBe('discord-token');
});
it('should allow disabling Signal read receipts via env var', () => {
process.env.SIGNAL_PHONE_NUMBER = '+1234567890';
process.env.SIGNAL_READ_RECEIPTS = 'false';
const config: LettaBotConfig = {
server: { mode: 'cloud' },
agent: { name: 'TestBot', model: 'test' },
channels: {},
};
const agents = normalizeAgents(config);
expect(agents[0].channels.signal?.readReceipts).toBe(false);
});
it('should pick up allowedUsers from env vars for all channels', () => {
process.env.TELEGRAM_BOT_TOKEN = 'tg-token';
process.env.TELEGRAM_DM_POLICY = 'allowlist';

View File

@@ -374,6 +374,7 @@ export interface SignalConfig {
cliPath?: string; // Path to signal-cli binary (default: "signal-cli")
httpHost?: string; // Daemon HTTP host (default: "127.0.0.1")
httpPort?: number; // Daemon HTTP port (default: 8090)
readReceipts?: boolean; // Send read receipts for incoming messages (default: true)
selfChat?: boolean;
dmPolicy?: 'pairing' | 'allowlist' | 'open';
allowedUsers?: string[];
@@ -707,6 +708,7 @@ export function normalizeAgents(config: LettaBotConfig): AgentConfig[] {
channels.signal = {
enabled: true,
phone: process.env.SIGNAL_PHONE_NUMBER,
readReceipts: process.env.SIGNAL_READ_RECEIPTS !== 'false',
selfChat: process.env.SIGNAL_SELF_CHAT_MODE !== 'false',
dmPolicy: (process.env.SIGNAL_DM_POLICY as 'pairing' | 'allowlist' | 'open') || 'pairing',
allowedUsers: parseList(process.env.SIGNAL_ALLOWED_USERS),