@floomhq/signaldash 0.36.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -102,7 +102,7 @@ setup installs both from the same pinned npm package
102
102
  the human chose to execute:
103
103
 
104
104
  ```bash
105
- npx -y @floomhq/signaldash@0.36.0 <invite-code>
105
+ npx -y @floomhq/signaldash@0.38.0 <invite-code>
106
106
  ```
107
107
 
108
108
  Run that command in a terminal, not in an agent chat. Do not ask an agent to
@@ -132,6 +132,7 @@ SignalDash exposes:
132
132
  - `li_list_chats`
133
133
  - `li_read_messages(chat_id)`
134
134
  - `li_mark_read(chat_id)`
135
+ - `li_start_chat(member_ids, text, confirm?, approval_hash?, dry_run?)`
135
136
  - `li_send_message(chat_id?, text?, expected_watermark?, mark_read?, secretary_receipt_id?)`
136
137
  - `li_send_invitation(provider_id, note?, confirm?)`
137
138
  - `li_invitations_received(limit?, cursor?)`
@@ -163,6 +164,7 @@ SignalDash exposes:
163
164
  - `wa_mark_read(chat_id)`
164
165
  - `wa_get_attachment(chat_id, message_id, attachment_id)`
165
166
  - `wa_transcribe_voice(chat_id, message_id, attachment_id)`
167
+ - `wa_start_chat(member_ids, text, confirm?, approval_hash?, dry_run?)`
166
168
  - `wa_send_message(chat_id, text?, attachments?, expected_watermark?, mark_read?)`
167
169
  - `wa_delete_message(chat_id, message_id)`
168
170
  - `wa_delete_messages(messages)`
@@ -187,6 +189,24 @@ SignalDash exposes:
187
189
  Every operation runs through the hosted SignalDash backend. Agents never
188
190
  receive the Unipile access key.
189
191
 
192
+ `li_start_chat` and `wa_start_chat` are separate tools because LinkedIn and
193
+ WhatsApp expose different stable member-ID formats and spend different budget
194
+ lanes. Both use the same safety contract. A first call previews 1 to 10 exact
195
+ member IDs and the exact first message. A confirmed call requires the
196
+ payload-bound approval hash, resolves every member live, rejects protected or
197
+ suppressed contacts, and checks attendee-scoped provider evidence for an exact
198
+ existing member set. An existing match is returned without creating a chat or
199
+ sending the text. A new chat costs one action, independent of member count,
200
+ because the provider performs one irreversible conversation-and-message write.
201
+ Success requires readback of exactly one matching chat and exactly one own first
202
+ message. `dry_run:true` repeats the live preflight and stops before the provider
203
+ write, without consuming approval or budget. Database capability flags
204
+ `linkedin_chat_start` and `whatsapp_chat_start` disable the paths immediately.
205
+ When WhatsApp resolves an `@lid` alias to a canonical member ID, the first
206
+ preview returns `member_alias_requires_exact_id` and `resolved_member_id`.
207
+ Preview again with that exact ID before approval. SignalDash never substitutes
208
+ an unapproved member identity inside a confirmed write.
209
+
190
210
  The inspiration library also extracts LinkedIn post references already stored
191
211
  inside conversation `signals`. This path scans SQLite only and makes zero
192
212
  LinkedIn or Unipile calls. Feed URLs and post slugs become canonical activity
@@ -305,6 +325,16 @@ uses the same sender binding, action ledger, duplicate guard, daily budget, and
305
325
  provider-warning lock as immediate publishing. An interrupted or ambiguous
306
326
  execution fails closed and is never retried automatically.
307
327
 
328
+ `li_scheduled_posts` is the shared planning read across posts stored by
329
+ SignalDash and the configured Buffer LinkedIn channel. Every item names its
330
+ source, and the response reports independent completeness for SignalDash,
331
+ Buffer, and native LinkedIn. Unipile has no documented endpoint for posts or
332
+ drafts scheduled natively in LinkedIn, so that source is always explicit as
333
+ `unavailable`. SignalDash does not call undocumented Voyager routes or automate
334
+ linkedin.com to fill the gap. An empty item list therefore means only that the
335
+ visible SignalDash and Buffer sources are empty, never that the native LinkedIn
336
+ calendar is empty.
337
+
308
338
  `sd_schedule_message` puts one exact message into one exact chat at one exact
309
339
  time, on WhatsApp or LinkedIn, text only. It enqueues; it never sends. The
310
340
  attachment bytes a WhatsApp send accepts are refused here rather than held on
@@ -361,9 +391,10 @@ explicitly enables it with `confirm:true`. The optional filters are an exact
361
391
  public-identifier allowlist and description-keyword matches; when both groups
362
392
  are configured, both must match. The single backend worker reads one bounded
363
393
  received-invitation page, accepts at most five invitations per run and ten per
364
- UTC day by default, jitters every action, and also consumes the shared daily
365
- LinkedIn action budget. Three consecutive parse or provider errors disable the
366
- setting. `sd_auto_accept_status` exposes accepted counts today and this week,
394
+ UTC day by default, jitters every action, and also consumes the invitation
395
+ daily lane and aggregate LinkedIn brake. Three consecutive parse or provider
396
+ errors disable the setting. `sd_auto_accept_status` exposes accepted counts
397
+ today and this week,
367
398
  failed attempts today, remaining capacity, the stop reason, and sanitized
368
399
  records for invitations whose provider shape could not be parsed. Both the current nested
369
400
  `specifics.shared_secret` and legacy top-level shape are supported; secrets are
@@ -415,22 +446,30 @@ npx -y @floomhq/signaldash connections linkedin-connections.csv
415
446
 
416
447
  ## Safety limits
417
448
 
418
- SignalDash enforces a daily send cap. The operating skill adds the human
449
+ SignalDash enforces daily write caps. The operating skill adds the human
419
450
  workflow around that runtime control:
420
451
 
421
- - Every successful send includes `rate_limit.limit`, `used`, `remaining`, and
422
- `resets_at`, plus matching `X-RateLimit-*` headers. When the per-user cap is
423
- exhausted, SignalDash returns HTTP 429 with `code: "rate_limit_exceeded"` and
424
- `Retry-After`. Usage is persisted server-side and resets at midnight UTC.
425
- - The caps are per channel lane: LinkedIn actions, WhatsApp and email sends
426
- together, and message deletes each spend their own daily budget. A `429` names
452
+ - Every successful LinkedIn write includes aggregate `rate_limit` data and its
453
+ applicable `lane_rate_limit` when classified. `rate_limit.limit`, `used`,
454
+ `remaining`, and `resets_at` retain their stable response shape. A lane
455
+ refusal returns HTTP
456
+ 429 with an exact invitation, message, or engagement code and
457
+ `rate_limit_lane`; the aggregate brake retains
458
+ `code: "rate_limit_exceeded"`. Every 429 includes `Retry-After`.
459
+ Counters are persisted server-side and reset at midnight UTC.
460
+ - LinkedIn uses three per-sender lanes: 20 invitation actions, 30 messages in
461
+ existing threads, and 50 replies or reactions on own posts per UTC day by
462
+ default. Publishing and first comments count with own-post engagement. A
463
+ separate aggregate cap of 75 is the emergency brake across the lanes. Sent
464
+ invitations retain the unchanged 100-attempt UTC-week policy.
465
+ - WhatsApp and email sends together, and message deletes, retain their own
466
+ independent budgets. A `429` names
427
467
  the cap it hit; it is never a statement about another lane. Two caveats worth
428
468
  knowing: on the day a deployment first upgrades past the lane split, that
429
469
  day's existing count is still read on the LinkedIn side (it is ambiguous, and
430
470
  the conservative reading is the safe one) and clears at the next UTC midnight;
431
- and the WhatsApp/email lane is keyed per access token while the LinkedIn
432
- ledger is keyed per user, so several live tokens for one user get one LinkedIn
433
- budget but a WhatsApp budget each.
471
+ LinkedIn, WhatsApp/email, and delete ledgers are keyed to the stable logical
472
+ user or sender rather than multiplied by live bearer sessions.
434
473
  - Read the exact thread before every send.
435
474
  - Preview an exact invitation target and note before sending it.
436
475
  - List the exact received or sent invitation before accepting or withdrawing it.
@@ -439,7 +478,8 @@ workflow around that runtime control:
439
478
  - Never infer a recipient from a partial name.
440
479
  - Never send a duplicate or retry an ambiguous timeout without re-reading.
441
480
  - Invitation auto-accept remains off until explicitly enabled, consumes both
442
- its dedicated daily cap and the shared LinkedIn action cap, and disables
481
+ its dedicated daily cap, the invitation lane, and the aggregate LinkedIn
482
+ brake, and disables
443
483
  itself after three consecutive parse or provider errors.
444
484
  - Do not parallelize sends or work around a rate limit.
445
485
  - Do not bypass paid-discovery previews, pacing, request caps, or cost caps.
@@ -449,8 +489,8 @@ workflow around that runtime control:
449
489
 
450
490
  LinkedIn invitation writes are single-object actions. SignalDash serializes
451
491
  them with other LinkedIn writes, requires an exact preview or list read,
452
- applies the shared daily action cap, and enforces a separate 100-attempt weekly
453
- policy by default. For a new invitation, jitter completes before the final
492
+ applies the invitation daily lane and aggregate brake, and enforces a separate
493
+ 100-attempt weekly policy by default. For a new invitation, jitter completes before the final
454
494
  preflight and action reservation; that preflight verifies relationship state,
455
495
  pending invitations in both directions, and absence of an existing one-to-one
456
496
  chat. Provider timeouts become non-retryable `outcome_unknown` locks.
package/bin/sd.mjs CHANGED
@@ -491,6 +491,23 @@ const TOOLS = [
491
491
  additionalProperties: false,
492
492
  },
493
493
  },
494
+ {
495
+ name: "li_start_chat",
496
+ path: "/li/start_chat",
497
+ description: "Preview or start one LinkedIn direct or group chat with 1 to 10 exact provider member IDs and one exact first message. First call without confirm. After human approval of the complete member set and text, repeat the exact payload with confirm:true and approval_hash. dry_run:true runs the full live preflight twice and stops immediately before the provider write. One created chat spends one thread-message and aggregate action regardless of member count. Existing exact member sets are returned without creating or sending anything.",
498
+ inputSchema: {
499
+ type: "object",
500
+ properties: {
501
+ member_ids: { type: "array", minItems: 1, maxItems: 10, uniqueItems: true, items: { type: "string", minLength: 3, maxLength: 500, pattern: "^(?:ACo|ACw|AE)[A-Za-z0-9_-]+$" } },
502
+ text: { type: "string", minLength: 1, maxLength: 5000 },
503
+ confirm: { type: "boolean", default: false },
504
+ approval_hash: { type: "string", minLength: 64, maxLength: 64, pattern: "^[0-9a-f]{64}$" },
505
+ dry_run: { type: "boolean", const: true },
506
+ },
507
+ required: ["member_ids", "text"],
508
+ additionalProperties: false,
509
+ },
510
+ },
494
511
  {
495
512
  name: "li_send_message",
496
513
  ch: "li",
@@ -637,7 +654,7 @@ const TOOLS = [
637
654
  {
638
655
  name: "sd_budget_status",
639
656
  path: "/sd/budget_status",
640
- description: "Read authoritative SignalDash LinkedIn attempt capacity for this UTC day and week across manual work and all campaigns. Reports the campaign-excluded portion without promising a reserved manual slot.",
657
+ description: "Read authoritative SignalDash LinkedIn attempt capacity for the invitation, existing-thread message, and own-post engagement daily lanes, the aggregate emergency brake, and the unchanged weekly invitation cap.",
641
658
  inputSchema: {
642
659
  type: "object",
643
660
  properties: {},
@@ -948,7 +965,7 @@ const TOOLS = [
948
965
  {
949
966
  name: "sd_campaign_preview",
950
967
  path: "/sd/campaign/preview",
951
- description: "Inspect one campaign before approval: every exact recipient, every exclusion and its reason, the exact message text for each step, the timing, the shared daily budget, and the approval_url to send the human. Show the human this content. This read also authorizes a later exact cancel. An agent cannot approve a campaign; only the authenticated human page can, and it can hand you a one-time code for sd_campaign_approve.",
968
+ description: "Inspect one campaign before approval: every exact recipient, every exclusion and its reason, the exact message text for each step, the timing, the invitation and existing-thread message lanes, the aggregate brake, and the approval_url to send the human. Show the human this content. This read also authorizes a later exact cancel. An agent cannot approve a campaign; only the authenticated human page can, and it can hand you a one-time code for sd_campaign_approve.",
952
969
  inputSchema: {
953
970
  type: "object",
954
971
  properties: {
@@ -1205,6 +1222,23 @@ const TOOLS = [
1205
1222
  additionalProperties: false,
1206
1223
  },
1207
1224
  },
1225
+ {
1226
+ name: "wa_start_chat",
1227
+ path: "/wa/start_chat",
1228
+ description: "Preview or start one WhatsApp direct or group chat with 1 to 10 exact @s.whatsapp.net or @lid member IDs and one exact first message. First call without confirm. If the provider maps an @lid alias to a canonical ID, repeat the preview with the returned resolved_member_id so the human approves the exact provider-write identity. After human approval of the complete member set and text, repeat the exact payload with confirm:true and approval_hash. dry_run:true runs the full live preflight twice and stops immediately before the provider write. One created chat spends one WhatsApp/email send action regardless of member count. Existing exact member sets are returned without creating or sending anything.",
1229
+ inputSchema: {
1230
+ type: "object",
1231
+ properties: {
1232
+ member_ids: { type: "array", minItems: 1, maxItems: 10, uniqueItems: true, items: { type: "string", minLength: 5, maxLength: 500, pattern: "^[A-Za-z0-9._-]+@(?:s\\.whatsapp\\.net|lid)$" } },
1233
+ text: { type: "string", minLength: 1, maxLength: 5000 },
1234
+ confirm: { type: "boolean", default: false },
1235
+ approval_hash: { type: "string", minLength: 64, maxLength: 64, pattern: "^[0-9a-f]{64}$" },
1236
+ dry_run: { type: "boolean", const: true },
1237
+ },
1238
+ required: ["member_ids", "text"],
1239
+ additionalProperties: false,
1240
+ },
1241
+ },
1208
1242
  {
1209
1243
  name: "wa_send_message",
1210
1244
  ch: "wa",
@@ -1529,7 +1563,7 @@ const TOOLS = [
1529
1563
  {
1530
1564
  name: "li_scheduled_posts",
1531
1565
  path: "/li/scheduled_posts",
1532
- description: "List this authenticated user's scheduled LinkedIn posts and their current states.",
1566
+ description: "Read the shared LinkedIn content calendar from SignalDash and the configured Buffer channel. Always inspect completeness and per-source state. Native LinkedIn scheduled posts and drafts are NOT visible because Unipile has no documented read route for them, and SignalDash does not use raw Voyager routes or linkedin.com browser access. An empty items array is never proof that the native LinkedIn calendar is empty.",
1533
1567
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
1534
1568
  },
1535
1569
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/signaldash",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "description": "Secure LinkedIn, WhatsApp, and email access for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "scripts": {
20
20
  "test": "node --test",
21
- "check": "node --check bin/sd.mjs && node --check server/server.cjs && node --check server/composio-email.cjs && node --check server/attachments.cjs && node --check server/write-control.cjs && node --check server/secretary-store.cjs && node --check server/secretary-collector.cjs && node --check server/secretary-latest.cjs && node --check server/secretary-approvals.cjs && node --check server/secretary-push.cjs && node --check server/secretary-rules.cjs && node --check server/secretary-shadow-gate.cjs && node --check server/secretary-vault-importer.cjs && node --check server/secretary-inspiration.cjs && node --check server/linkedin-archive-importer.cjs && node --check server/voice-profile.cjs && node --check server/campaign-store.cjs && node --check server/campaign-runner.cjs && node --check server/withdrawal-store.cjs && node --check server/post-scheduler.cjs && node --check server/message-scheduler.cjs && node --check scripts/import-linkedin-archive.mjs && node --check scripts/import-secretary-vault-snapshot.mjs && node --check scripts/rebuild-secretary-actors.mjs && node --check scripts/extract-conversation-inspiration.mjs && node --check bin/signaldash.js && node --check lib/cli.js && node --check lib/config-file.js && node --check lib/mcp.js && node --check lib/rate-guard.js && node --check lib/secrets.js && node --check lib/unipile.js"
21
+ "check": "node --check bin/sd.mjs && node --check server/server.cjs && node --check server/buffer-calendar.cjs && node --check server/composio-email.cjs && node --check server/attachments.cjs && node --check server/write-control.cjs && node --check server/secretary-store.cjs && node --check server/secretary-collector.cjs && node --check server/secretary-latest.cjs && node --check server/secretary-approvals.cjs && node --check server/secretary-push.cjs && node --check server/secretary-rules.cjs && node --check server/secretary-shadow-gate.cjs && node --check server/secretary-vault-importer.cjs && node --check server/secretary-inspiration.cjs && node --check server/linkedin-archive-importer.cjs && node --check server/voice-profile.cjs && node --check server/campaign-store.cjs && node --check server/campaign-runner.cjs && node --check server/withdrawal-store.cjs && node --check server/post-scheduler.cjs && node --check server/message-scheduler.cjs && node --check scripts/import-linkedin-archive.mjs && node --check scripts/import-secretary-vault-snapshot.mjs && node --check scripts/rebuild-secretary-actors.mjs && node --check scripts/extract-conversation-inspiration.mjs && node --check bin/signaldash.js && node --check lib/cli.js && node --check lib/config-file.js && node --check lib/mcp.js && node --check lib/rate-guard.js && node --check lib/secrets.js && node --check lib/unipile.js"
22
22
  },
23
23
  "keywords": [
24
24
  "mcp",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: signaldash
3
- description: Operate the user's SignalDash connection to LinkedIn, WhatsApp, and email. Use this skill whenever the user says or implies "check my LinkedIn", "who messaged me", "triage my inbox", "read the thread with X", "reply to X", "send this message", "send a connection request", "create an invitation batch", "run a campaign", "connect and then message them", "message them when they accept", "check invitations", "accept this invitation", "auto-accept my invitations", "check auto-accept status", "withdraw this invitation", "search my LinkedIn connections", "find people in my network", "find people beyond my network", "suppress this contact", "check contact state", "check my action budget", "check WhatsApp", "check my email", "draft a LinkedIn post", "who engaged with my post", "who liked or commented", or "export my LinkedIn connections". Covers durable installation, invite-based setup, local-first connection search, capped paid discovery, immutable human-approved invitation batches, the human-approved campaign loop that messages a person only after a proven acceptance and stops on any reply, opt-in rate-capped invitation auto-accept, exact contact state and suppression, authoritative LinkedIn action-budget status, safe reading, drafting, approved sends, exact invitation actions, post engagement analysis, and paced connection export.
3
+ description: Operate the user's SignalDash connection to LinkedIn, WhatsApp, and email. Use this skill whenever the user says or implies "check my LinkedIn", "who messaged me", "triage my inbox", "read the thread with X", "reply to X", "send this message", "start a chat", "create a group chat", "introduce these people", "send a connection request", "create an invitation batch", "run a campaign", "connect and then message them", "message them when they accept", "check invitations", "accept this invitation", "auto-accept my invitations", "check auto-accept status", "withdraw this invitation", "search my LinkedIn connections", "find people in my network", "find people beyond my network", "suppress this contact", "check contact state", "check my action budget", "check WhatsApp", "check my email", "draft a LinkedIn post", "who engaged with my post", "who liked or commented", or "export my LinkedIn connections". Covers durable installation, invite-based setup, exact human-approved direct and group chat creation, local-first connection search, capped paid discovery, immutable human-approved invitation batches, the human-approved campaign loop that messages a person only after a proven acceptance and stops on any reply, opt-in rate-capped invitation auto-accept, exact contact state and suppression, authoritative LinkedIn action-budget status, safe reading, drafting, approved sends, exact invitation actions, post engagement analysis, and paced connection export.
4
4
  ---
5
5
 
6
6
  # Operate SignalDash
@@ -375,6 +375,7 @@ Use the exact tool names and argument keys below. Limits are optional.
375
375
  | `li_list_chats` | `limit` integer 1-100, default 20; `cursor` optional; `search` optional string, max 200 characters; `max_scan` optional integer 1-500; `unread` optional boolean | Find recent LinkedIn chats, unread counts, and exact `chat_id` values. `unread:true` is forwarded to the provider as `unread=true`; SignalDash does not fetch a full chat page and filter it locally. Search accepts a unique stored display name, exact `public_id`, member id, or ordinary chat field. A stored name/public id is resolved to its verified member id before matching, so a provider row with `name:null` remains findable. Without an explicit `max_scan`, a zero-match search extends from 200 up to a hard 500-chat/five-page bound; explicit bounds remain exact. Read `scanned_chats`, `pages_fetched`, `scan_limit`, and `exhaustive` before concluding absence. The response also reports any resolved public/member ids. Any other key, including `text` and `member_id`, is refused with `unsupported_parameter` rather than accepted and ignored. |
376
376
  | `li_read_messages` | `chat_id` required; `limit` 1-100, default 30 | Read one resolved LinkedIn conversation before summarizing, drafting, or sending. The response includes `current_watermark`, the resolved `chat`, and `sender_name` on each message where the exact sender identity can be resolved. |
377
377
  | `li_mark_read` | `chat_id` required | Explicitly clear one LinkedIn chat's unread state. Reading never clears unread automatically. |
378
+ | `li_start_chat` | `member_ids` required array of 1-10 exact `ACo`, `ACw`, or `AE` provider member IDs; `text` required exact first message; `confirm`, `approval_hash`, and `dry_run:true` apply only after preview | Start one direct or group chat. Preview first, show the complete member-ID set and exact text, then repeat that exact payload with the returned approval hash only after explicit approval. SignalDash resolves every member live, blocks protected or suppressed contacts, returns an existing exact member-set chat without sending, and charges one thread-message plus aggregate action for a new chat regardless of member count. `dry_run:true` proves the full path up to the provider write without consuming approval or budget. |
378
379
  | `li_send_message` | Manual path: `chat_id` and `text` required; `expected_watermark` optional exact 64-character watermark; `mark_read` optional boolean. Secretary path: `secretary_receipt_id` alone. | Send one approved LinkedIn reply after an immediate read of that exact chat. A Secretary receipt derives the exact stored chat and text and refuses every caller-supplied override. |
379
380
  | `li_send_invitation` | `provider_id` required; `note` optional, max 300 exact characters; `confirm` optional, default false | First preview one exact target and note. The server verifies relationship, both invitation directions, and absence of an existing one-to-one chat. After exact approval, repeat the identical call with `confirm:true`; jitter completes before the final preflight and action reservation. |
380
381
  | `li_invitations_received` | `limit` integer 1-100, default 50; `cursor` optional | List one bounded page of received invitations. This read authorizes only the exact returned invitation IDs for a later accept. |
@@ -382,7 +383,7 @@ Use the exact tool names and argument keys below. Limits are optional.
382
383
  | `li_invitations_sent` | `limit` integer 1-100, default 50; `cursor` optional | List one bounded page of sent invitations. This read authorizes only the exact returned invitation IDs for a later withdrawal. |
383
384
  | `li_withdraw_invitation` | `invitation_id` required; `confirm:true` required | Withdraw one exact currently pending sent invitation after a fresh `li_invitations_sent` read and approval. |
384
385
  | `sd_contact_state` | `channel` required (`linkedin`, `whatsapp`, or `email`); `identifiers` required array of 1-8 exact `{kind,value}` objects; `action` optional (`get` default or `suppress`); suppression also requires an allowed `reason` and `confirm:true` | Inspect exact tenant/channel-scoped contact history or add a protective suppression. It never infers that identifiers on different channels belong to one person. |
385
- | `sd_budget_status` | no arguments | Read the current sender binding, daily total/manual/campaign/unknown attempts, total and combined-campaign capacity, weekly invitation usage, lock state, and UTC resets from the authoritative server ledger. |
386
+ | `sd_budget_status` | no arguments | Read the current sender binding, separate invitation, existing-thread message, and own-post engagement daily lanes, the aggregate emergency brake, weekly invitation usage, lock state, and UTC resets from the authoritative server ledger. |
386
387
  | `sd_settings_get` | no arguments | Read the authenticated user's persistent SignalDash settings. Auto-accept is the first supported setting and is false for every existing user until explicitly changed. |
387
388
  | `sd_settings_set` | `auto_accept_linkedin` required boolean; `auto_accept_linkedin_filters` optional object with `public_identifiers` and `description_keywords`; `confirm:true` required | Update the general per-user settings surface after exact human approval. This changes no future or unknown setting implicitly. |
388
389
  | `sd_auto_accept_status` | no arguments | Inspect whether auto-accept is enabled, its exact filters, accepted today and this week, failed attempts today, dedicated and shared capacity, repeated-error stop state, and sanitized unparseable invitation records. |
@@ -393,7 +394,7 @@ Use the exact tool names and argument keys below. Limits are optional.
393
394
  | `li_get_invitation_batch` | `batch_id` required | Inspect every stored target, exact note, exclusion, hash, timing, capacity, and result. Use the returned browser review path for human approval. This read also authorizes a later exact cancel. |
394
395
  | `li_cancel_invitation_batch` | `batch_id` required; `approval_view_hash` required string or null exactly as inspected; `confirm:true` required | Permanently cancel unstarted targets in one freshly inspected batch. It cannot recall an executing invitation, and restarting requires a new preview and approval. |
395
396
  | `sd_campaign_create` | `source_label` required, max 120 code points; `time_zone` required IANA timezone; `messages` required array of 1-5 exact `{text,after_days?}` objects, text max 1200 code points; `target_source` optional (`explicit` default or `post_engagers`); `targets` array of up to 150 `{profile_url?,provider_id?,inclusion_reason,note?,display_name?,headline?,adopt_existing_invitation?}` for `explicit` (at most 10 adopted targets per campaign, and an adopted target may not carry a `note`); `engagers` object with `post_limit` 1-10, `max_targets` 1-150, `inclusion_reason`, `note` for `post_engagers`; `invite_ttl_days` optional 1-60, default 21 | Create one campaign: a connection request to each exact person, then the exact approved message(s) once that person is proven to have accepted, then an optional follow-up that stops on any reply. The server never generates a target or a word of text. Nothing is sent before human approval. |
396
- | `sd_campaign_preview` | `campaign_id` required | Inspect every exact recipient, every exclusion and reason, the exact text of every message step, the timing, the shared daily budget, and the `approval_url` to hand the human. This read also authorizes a later exact cancel. |
397
+ | `sd_campaign_preview` | `campaign_id` required | Inspect every exact recipient, every exclusion and reason, the exact text of every message step, the timing, the invitation and existing-thread message lanes, the aggregate brake, and the `approval_url` to hand the human. This read also authorizes a later exact cancel. |
397
398
  | `sd_campaign_approve` | `campaign_id` required; `confirm_token` required in the exact `sd-xxxx-xxxx-xxxx` form the human read off the approval page | Record the human's approval using the single-use code that authenticated page minted for them. An agent cannot mint, guess, or bypass that code. |
398
399
  | `sd_campaign_status` | `campaign_id` required | Monitor per person: invited, acceptance proven, messaged, replied and halted, expired, or excluded, plus every message step and any provider response SignalDash could not parse. |
399
400
  | `sd_campaign_cancel` | `campaign_id` required; `approval_view_hash` required string or null exactly as inspected; `confirm:true` required | Revoke the approval and stop every unsent invitation and unsent message. It cannot recall an executing write, and restarting requires a new preview and approval. |
@@ -406,6 +407,7 @@ Use the exact tool names and argument keys below. Limits are optional.
406
407
  | `wa_mark_read` | `chat_id` required | Explicitly clear one WhatsApp chat's unread state. Reading never clears unread automatically. |
407
408
  | `wa_get_attachment` | `chat_id`, `message_id`, `attachment_id` all required, max 500 characters each | Download one attachment of one message in a chat this account owns. Read the chat first: the exact `message_id` and `attachment_id` come from `wa_read_messages`. Returns the stored path on the SignalDash host, mimetype, byte size and sha256. |
408
409
  | `wa_transcribe_voice` | `chat_id`, `message_id`, `attachment_id` all required, max 500 characters each; `backend` optional, exactly `gemini` or `whisper` | Turn one WhatsApp voice note into text through SignalDash instead of fetching provider bytes yourself. Always read the returned `backend`: `gemini` is the accurate default, `whisper-small` is the weak local fallback and mangles German with English terms mixed in, and a fallback also carries `fallback_reason`. Non-audio attachments are refused with `415 not_audio`; an unknown backend with `400 unknown_backend`; a transcription that exceeds its time limit returns `504 transcription_timeout` with the stored audio path. |
410
+ | `wa_start_chat` | `member_ids` required array of 1-10 exact `@s.whatsapp.net` or `@lid` provider member IDs; `text` required exact first message; `confirm`, `approval_hash`, and `dry_run:true` apply only after preview | Start one direct or group chat under the same exact approval, live member resolution, protected-contact, duplicate-set, and readback contract as LinkedIn. If an `@lid` resolves to another canonical ID, repeat the preview with the returned `resolved_member_id`; SignalDash never substitutes an unapproved identity. A new chat spends one WhatsApp/email send action regardless of member count. An existing exact member-set chat is returned without sending. |
409
411
  | `wa_send_message` | `chat_id` required; `text` optional only when a file is attached, max 5000 characters; `attachments` optional array of up to 4 exact `{filename, content_type, content_base64}` files, at most 16 MiB per file and 16 MiB per message, types `image/png`, `image/jpeg`, `image/webp`, `image/gif`, `application/pdf`, `text/csv`, `text/plain`, `application/json`, `application/zip`, xlsx; `expected_watermark` optional exact 64-character watermark; `mark_read` optional boolean, default false | Send one approved reply, one approved file, or both, in an existing WhatsApp conversation after an immediate re-read. Own outbound additions do not invalidate the read; inbound additions or mutations return `new_messages`, `changed_kind`, and `current_watermark`. Pass `expected_watermark` to bind the send to the exact reviewed state and `mark_read:true` only when the approved workflow also calls for clearing unread after the confirmed send. A file spends the same daily send budget and is recorded the same way as a text message; there is no separate attachment budget. A call carrying neither text nor an attachment is refused with `400 text_or_attachment_required`. Attachments are checked before anything is reserved, so a refusal costs no send: `400 unsupported_attachment_type`, `400 attachment_too_large`, `400 attachments_too_large`, `400 too_many_attachments`, `400 malformed_attachment_base64`, `400 invalid_attachment_filename`, and `413 request_too_large` when the whole body is too big to read. Nothing is ever truncated or dropped silently. The same caption with the same file is refused as `409 duplicate_send`; the same caption with a different file is a different message and goes through. If a send times out or the provider never confirms it, the message may still have been delivered: an identical retry is refused with `409 send_outcome_unknown`. Read the chat AGAIN, and ONLY if the message is genuinely absent, resend the identical payload with `confirm_resend:true`. The re-read is enforced, not advisory: a `confirm_resend` whose most recent read of that chat predates the failed attempt is refused with `428 reread_after_failed_send_required`, because a read taken before the attempt cannot show whether the message arrived. A re-read failure remains `502 thread_preflight_unavailable`, and a legacy proof with no watermark remains `428 read_before_send_required`; neither costs send budget. LinkedIn messages carry text only. |
410
412
  | `wa_delete_message` | `chat_id`, `message_id` both required, max 500 characters each | Retract one message THIS account sent, in a chat this account owns. The exact `message_id` comes from `wa_read_messages`. Irreversible and never retried: someone else's message is refused with `403 message_not_own`, a message outside this chat with `403 message_forbidden`, and a delete already recorded for this exact chat and message with `409 duplicate_delete`. Deletes spend their own daily budget, so `429 rate_limit_exceeded` here never means you are out of sends. WhatsApp applies its own time and role limits to deleting for everyone and can answer successfully without removing anything, so read the chat again to confirm the message is gone. |
411
413
  | `wa_delete_messages` | `messages` required array of 1-200 exact `{chat_id, message_id}` objects | Retract several messages this account sent. Same ownership, budget and audit path as `wa_delete_message`, executed strictly one at a time with a pause between them, never in parallel. Always read the per-entry `ok`, `code` and `error`: a partial result is normal. Entries the batch never reached before its time limit come back with `skipped:true` and `code:batch_deadline`, and were not attempted; resend exactly those to resume. |
@@ -421,7 +423,7 @@ Use the exact tool names and argument keys below. Limits are optional.
421
423
  | `li_delete_comment` | `post_id`, `comment_id`, and `confirm:true` required | Currently unavailable: the fixture-tested Unipile v2 wrapper is held at database capability state `untested` until live compatibility is proved against Federico's own removable comment. When enabled it proves own comment identity and readback. Deletion is remediation, not rollback. |
422
424
  | `li_draft_post` | `text` required, max 3000; `publish` optional; `scheduled_at` optional offset-qualified ISO date-time; `mentions` optional array of up to 20 exact `{name,profile_id}` objects; `attachments` optional array of up to 4 exact `{filename,content_type,content_base64}` images; `first_comment` optional, max 1250 | Create a server-confirmed draft, publish now, or persist an exact future LinkedIn post and its approved first comment. A schedule is stored only with `publish:true` after approval of the exact text, time, mentions, images, and comment. |
423
425
  | `li_set_scheduled_post_first_comment` | `id` required UUID; `first_comment` required, max 1250; `confirm:true` required | Attach one exact approved first comment to a scheduled post. SignalDash publishes it through the same connected account after the post and never republishes the post if the comment fails. |
424
- | `li_scheduled_posts` | no arguments | List only this authenticated user's scheduled LinkedIn posts and their durable states. |
426
+ | `li_scheduled_posts` | no arguments | Read one shared content-calendar view across SignalDash and the configured Buffer LinkedIn channel. Inspect `completeness` and every `sources.*.state` before treating absence as an empty calendar. Native LinkedIn scheduled posts and drafts are invisible because Unipile has no documented read route for them; SignalDash does not use raw Voyager routes or linkedin.com browser access. An empty `items` array proves only that the visible sources returned no entries. |
425
427
  | `li_cancel_scheduled_post` | `id` required UUID; `confirm:true` required | Cancel one exact post while it is still scheduled. It cannot recall an executing or published post. |
426
428
  | `sd_schedule_message` | `channel` required, `whatsapp` or `linkedin`; `chat_id` required, max 500; `text` required, max 5000; `scheduled_at` required offset-qualified ISO date-time from 60 seconds to 365 days ahead; `confirm:true` required | Schedule one exact message into one chat you have already read. Read that exact chat first, at `limit` 10 or more on LinkedIn: SignalDash records what the thread looked like and refuses at send time if the conversation moved. Text only; attachments are refused rather than dropped. One message at one time, never a sequence. |
427
429
  | `sd_scheduled_messages` | `state` optional, one of `scheduled`, `executing`, `sent`, `cancelled`, `failed`, `needs_review`; `channel` optional | List only this authenticated user's scheduled messages and their durable states. Returns every matching row, unpaginated, and always reports `needs_review_count` outside your filter. |
@@ -436,6 +438,7 @@ sd_secretary_push_set({"enabled":false,"confirm":true})
436
438
  sd_inspiration_list({})
437
439
  li_list_chats({"limit":20})
438
440
  li_read_messages({"chat_id":"chat_li_7f3a","limit":20})
441
+ li_start_chat({"member_ids":["ACoAAExactMemberOne","ACoAAExactMemberTwo"],"text":"Amina, meet Jo. I think you two have a lot to compare."})
439
442
  li_send_message({"chat_id":"chat_li_7f3a","text":"Yes. I’ll send it this afternoon."})
440
443
  li_send_invitation({"provider_id":"ACoAAExactMember","note":"Hi Amina, I enjoyed your post on agent safety."})
441
444
  li_send_invitation({"provider_id":"ACoAAExactMember","note":"Hi Amina, I enjoyed your post on agent safety.","confirm":true})
@@ -467,6 +470,7 @@ wa_list_chats({"limit":20})
467
470
  wa_read_messages({"chat_id":"chat_wa_91b2","limit":20})
468
471
  wa_get_attachment({"chat_id":"chat_wa_91b2","message_id":"msg_wa_5c71","attachment_id":"att_wa_0a33"})
469
472
  wa_transcribe_voice({"chat_id":"chat_wa_91b2","message_id":"msg_wa_5c71","attachment_id":"att_wa_0a33"})
473
+ wa_start_chat({"member_ids":["4915111111111@s.whatsapp.net","202700000000000@lid"],"text":"Amina, meet Jo. I think you two have a lot to compare."})
470
474
  wa_send_message({"chat_id":"chat_wa_91b2","text":"16:30 works. See you then."})
471
475
  wa_send_message({"chat_id":"chat_wa_91b2","text":"Q3 numbers attached.","attachments":[{"filename":"q3-arr.csv","content_type":"text/csv","content_base64":"<base64>"}]})
472
476
  wa_delete_message({"chat_id":"chat_wa_91b2","message_id":"msg_wa_5c71"})
@@ -510,7 +514,11 @@ permission to publish or schedule. For a future post, first call
510
514
  offset-qualified UTC instant, mentions, attachment count, and first comment.
511
515
  After explicit
512
516
  approval, repeat the identical payload with `publish:true`. Verify the stored
513
- record with `li_scheduled_posts`. For an existing scheduled post, use
517
+ record with `li_scheduled_posts`. Before scheduling, read that shared view and
518
+ inspect its `completeness` plus the SignalDash, Buffer, and native LinkedIn
519
+ source states. A date conflict in either visible source blocks planning until
520
+ the human resolves it. Native LinkedIn remains explicitly blind, so never call
521
+ an empty result an empty calendar. For an existing scheduled post, use
514
522
  `li_set_scheduled_post_first_comment` only after approval of the exact comment.
515
523
  SignalDash persists the published post ID before sending the comment, so a
516
524
  comment failure never republishes the post. It fails interrupted or ambiguous
@@ -553,14 +561,24 @@ with `confirm:true`. Allowed reasons are `opt_out`, `not_interested`,
553
561
  manual LinkedIn work or any campaign action. It reports attempts that
554
562
  SignalDash has committed in its authoritative SQLite ledger:
555
563
 
556
- - the total daily cap (`H`);
564
+ - invitations, including accept and single-object withdrawal maintenance;
565
+ - messages into existing threads;
566
+ - replies, reactions, publishing, and first comments on the sender's own posts;
567
+ - the aggregate daily emergency brake (`H`);
557
568
  - the campaign-excluded portion (`R = min(4, H)`);
558
569
  - the combined ceiling across all campaigns (`C = H - R`);
559
570
  - total, manual, campaign, and unknown attempts for the UTC policy day;
560
- - raw total and combined-campaign capacity remaining;
571
+ - raw per-lane, aggregate, and combined-campaign capacity remaining;
561
572
  - actions available now after connection and sender-lock state;
562
573
  - weekly LinkedIn invitation usage and its Monday 00:00 UTC reset.
563
574
 
575
+ The code defaults are 20 invitation actions, 30 existing-thread messages, and
576
+ 50 own-post engagement actions per UTC day. A separate aggregate cap of 75 is
577
+ the emergency brake across them. The three lane caps do not add into a promise
578
+ of 100 actions. Every lane still uses the same serialized provider queue and
579
+ its existing action-specific randomized pacing. The default 100-attempt UTC
580
+ week for sent invitations remains separate and unchanged.
581
+
564
582
  `R` is capacity campaigns cannot consume. It is not a reserve or guaranteed
565
583
  manual allowance: manual activity can consume the total cap first, and a later
566
584
  manual action can still be rejected at `H`. Campaign capacity is shared across
@@ -1270,10 +1288,14 @@ public LinkedIn posts.
1270
1288
  9. Confirm delivery by reading the thread after the action.
1271
1289
  10. Account health outranks throughput and task completion.
1272
1290
 
1273
- The hosted backend defaults to 20 LinkedIn action attempts per stable logical
1274
- sender per UTC day, shared across messages, invitations, invitation
1275
- maintenance, and post publishing. Invitation sends also use the default
1276
- 100-attempt weekly policy.
1291
+ The hosted backend defaults to three LinkedIn lanes per stable logical sender
1292
+ and UTC day: 20 invitation actions, 30 messages in existing threads, and 50
1293
+ own-post engagement actions. An aggregate 75-attempt emergency brake bounds the
1294
+ combined lanes. Invitation sends also use the unchanged default 100-attempt
1295
+ weekly policy. Separate counters do not alter randomized action spacing, the
1296
+ single serialized provider queue, duplicate protection, or provider-warning
1297
+ locks. An exact comment reply text already submitted by that sender on the same
1298
+ UTC day is refused as `duplicate_comment_text`.
1277
1299
 
1278
1300
  WhatsApp and email share ONE separate daily bucket, and it is independent of
1279
1301
  the LinkedIn one: a WhatsApp message never spends LinkedIn capacity and a
@@ -1750,8 +1772,10 @@ Before reporting completion, verify:
1750
1772
  - No sender lock or `outcome_unknown` result was retried.
1751
1773
  - A send was confirmed by a post-send read.
1752
1774
  - A post draft remained unpublished and unscheduled unless `publish:true` was
1753
- explicitly approved. Every scheduled post was read back with its exact UTC
1754
- instant, mention count, attachment count, first comment, and durable states.
1775
+ explicitly approved. The shared calendar was read first, its source-specific
1776
+ completeness was reported, and every visible scheduled post was read back
1777
+ with its exact UTC instant, source, and durable state. Native LinkedIn
1778
+ blindness was never represented as an empty calendar.
1755
1779
  - An export ended with `+ saved` and a non-empty CSV.
1756
1780
 
1757
1781
  If any item is unverified, state exactly what remains incomplete.
@@ -77,15 +77,20 @@ On no, call `sd_secretary_reject` for that exact disposition and send nothing.
77
77
  Inspect `already_in_contact`, `contact_state`, and `blocking_reasons`.
78
78
  Resolve, review, and approve every later action through its one-object flow.
79
79
  - Read `sd_budget_status` before planning multiple LinkedIn actions. Treat its
80
- campaign-excluded portion as capacity campaigns cannot consume, never as a
81
- reserved or guaranteed manual allowance. Its counts cover SignalDash
82
- attempts, not all native LinkedIn activity.
83
- - Keep normal messaging at human volume. Use the cap reported in each hosted
84
- response; LinkedIn messages, invitations, invitation maintenance, and post
85
- publishing share the stable sender's daily action budget.
80
+ invitation, existing-thread message, and own-post engagement lanes
81
+ separately. Treat the aggregate cap and campaign-excluded portion as brakes,
82
+ never as a promised allowance. Its counts cover SignalDash attempts, not all
83
+ native LinkedIn activity.
84
+ - Keep normal messaging at human volume. Code defaults are 20 invitation
85
+ actions, 30 existing-thread messages, 50 own-post engagement actions, and a
86
+ 75-attempt aggregate brake per UTC day. The unchanged sent-invitation policy
87
+ also caps attempts at 100 per UTC week.
86
88
  - Let SignalDash serialize LinkedIn writes and apply action-specific jitter. Do
87
89
  not parallelize write calls or bypass the guard. Invitation jitter completes
88
90
  before the server's final provider preflight and action reservation.
91
+ - Never reuse exact comment reply text during the same UTC day. SignalDash
92
+ refuses it as `duplicate_comment_text`; changing punctuation only to evade
93
+ that guard is prohibited.
89
94
  - Campaign invitations use a sender-local Monday-Friday 09:00-17:00 work
90
95
  window and a durable 90-180 second per-sender pacing interval. Timing is
91
96
  checked before final preflight and atomically during action reservation.