@desplega.ai/agent-swarm 1.85.0 → 1.86.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.
Files changed (93) hide show
  1. package/README.md +1 -0
  2. package/openapi.json +1 -1
  3. package/package.json +8 -6
  4. package/src/be/db.ts +44 -0
  5. package/src/be/migrations/078_backfill_gpt_5_5_pricing.sql +15 -0
  6. package/src/be/modelsdev-cache.json +152028 -0
  7. package/src/be/modelsdev-cache.ts +46 -0
  8. package/src/be/seed-pricing.ts +7 -44
  9. package/src/cli.tsx +12 -2
  10. package/src/commands/codex-session-runner.ts +132 -0
  11. package/src/commands/credential-wait.ts +2 -2
  12. package/src/commands/provider-credentials.ts +10 -5
  13. package/src/commands/runner.ts +3 -3
  14. package/src/prompts/base-prompt.ts +49 -3
  15. package/src/providers/claude-adapter.ts +83 -2
  16. package/src/providers/claude-managed-models.ts +18 -2
  17. package/src/providers/codex-adapter.ts +417 -97
  18. package/src/providers/codex-models.ts +9 -2
  19. package/src/providers/index.ts +28 -19
  20. package/src/providers/pricing-sources.md +7 -4
  21. package/src/providers/swarm-events-shared.ts +14 -0
  22. package/src/slack/HEURISTICS.md +5 -1
  23. package/src/slack/handlers.test.ts +35 -0
  24. package/src/slack/handlers.ts +79 -2
  25. package/src/tests/base-prompt.test.ts +46 -8
  26. package/src/tests/claude-managed-adapter.test.ts +4 -4
  27. package/src/tests/codex-adapter-otel.test.ts +4 -4
  28. package/src/tests/codex-adapter.test.ts +20 -7
  29. package/src/tests/codex-swarm-events.test.ts +35 -0
  30. package/src/tests/context-window.test.ts +1 -0
  31. package/src/tests/credential-check.test.ts +48 -29
  32. package/src/tests/entrypoint-config-env-export.test.ts +81 -0
  33. package/src/tests/follow-up-redelivery-guard.test.ts +165 -0
  34. package/src/tests/migration-046-budgets.test.ts +6 -5
  35. package/src/tests/pricing-routes.test.ts +6 -5
  36. package/src/tests/provider-adapter.test.ts +10 -10
  37. package/src/tests/provider-command-format.test.ts +4 -4
  38. package/src/tests/session-costs-codex-recompute.test.ts +25 -0
  39. package/src/tools/send-task.ts +30 -9
  40. package/src/utils/context-window.ts +1 -0
  41. package/templates/schedules/daily-blocker-digest/config.json +13 -0
  42. package/templates/schedules/daily-blocker-digest/content.md +150 -0
  43. package/templates/schedules/daily-compounding-reflection/config.json +21 -0
  44. package/templates/schedules/daily-compounding-reflection/content.md +210 -0
  45. package/templates/schedules/daily-hn-briefing/config.json +13 -0
  46. package/templates/schedules/daily-hn-briefing/content.md +97 -0
  47. package/templates/schedules/daily-workflow-health-audit/config.json +13 -0
  48. package/templates/schedules/daily-workflow-health-audit/content.md +189 -0
  49. package/templates/schedules/gtm-weekly-review/config.json +13 -0
  50. package/templates/schedules/gtm-weekly-review/content.md +58 -0
  51. package/templates/schedules/weekly-dependabot-triage/config.json +13 -0
  52. package/templates/schedules/weekly-dependabot-triage/content.md +45 -0
  53. package/templates/schema.ts +26 -0
  54. package/templates/skills/agentmail-sending/config.json +13 -0
  55. package/templates/skills/agentmail-sending/content.md +48 -0
  56. package/templates/skills/artifacts/config.json +13 -0
  57. package/templates/skills/artifacts/content.md +87 -0
  58. package/templates/skills/browser-use-cloud/config.json +13 -0
  59. package/templates/skills/browser-use-cloud/content.md +155 -0
  60. package/templates/skills/desloppify/config.json +13 -0
  61. package/templates/skills/desloppify/content.md +201 -0
  62. package/templates/skills/exa-search/config.json +13 -0
  63. package/templates/skills/exa-search/content.md +106 -0
  64. package/templates/skills/jira-interaction/config.json +13 -0
  65. package/templates/skills/jira-interaction/content.md +252 -0
  66. package/templates/skills/kapso-whatsapp/config.json +13 -0
  67. package/templates/skills/kapso-whatsapp/content.md +369 -0
  68. package/templates/skills/kv-storage/config.json +13 -0
  69. package/templates/skills/kv-storage/content.md +111 -0
  70. package/templates/skills/linear-interaction/config.json +20 -0
  71. package/templates/skills/linear-interaction/content.md +230 -0
  72. package/templates/skills/pages/config.json +18 -0
  73. package/templates/skills/pages/content.md +85 -0
  74. package/templates/skills/profile-corruption-escalation/config.json +13 -0
  75. package/templates/skills/profile-corruption-escalation/content.md +105 -0
  76. package/templates/skills/scheduled-task-resilience/config.json +13 -0
  77. package/templates/skills/scheduled-task-resilience/content.md +95 -0
  78. package/templates/skills/sprite-cli/config.json +13 -0
  79. package/templates/skills/sprite-cli/content.md +133 -0
  80. package/templates/skills/turso-interaction/config.json +13 -0
  81. package/templates/skills/turso-interaction/content.md +192 -0
  82. package/templates/skills/workflow-iterate/config.json +18 -0
  83. package/templates/skills/workflow-iterate/content.md +399 -0
  84. package/templates/skills/workflow-structured-output/config.json +13 -0
  85. package/templates/skills/workflow-structured-output/content.md +101 -0
  86. package/templates/skills/x-api-interactions/config.json +13 -0
  87. package/templates/skills/x-api-interactions/content.md +109 -0
  88. package/templates/workflows/autopilot/config.json +13 -0
  89. package/templates/workflows/autopilot/content.md +58 -0
  90. package/templates/workflows/linear-drain-loop/config.json +21 -0
  91. package/templates/workflows/linear-drain-loop/content.md +72 -0
  92. package/templates/workflows/ralph-loop/config.json +13 -0
  93. package/templates/workflows/ralph-loop/content.md +75 -0
@@ -0,0 +1,369 @@
1
+ # Kapso WhatsApp
2
+
3
+ Kapso (https://kapso.ai) is the WhatsApp platform vendor we use for the Meta Cloud API. The swarm has one phone number provisioned and either a native inbound handler (PR #560) or a workflow wired to dispatch a task per inbound message.
4
+
5
+ ## When to use MCP tools vs this skill's REST recipes
6
+
7
+ PR #560 ships **thin MCP-tool wrappers for the common case only**:
8
+
9
+ | Tool | Use for |
10
+ |---|---|
11
+ | `send-whatsapp-message` | Free-form text within the 24h session window. |
12
+ | `reply-whatsapp-message` | Same as above but quote-threads to an inbound WAMID. |
13
+ | `register-kapso-number` / `unregister-kapso-number` | Provisioning a phone number's webhook + KV mapping. |
14
+
15
+ **For ANYTHING else, drop to the REST recipes in this skill** — these are the canonical reference, and the MCP tools deliberately do NOT duplicate them:
16
+
17
+ - **Template messages** (outside 24h window) → §"Send a template" below.
18
+ - **Media** (image / document / audio / video, including wide-image padding and PTT voice notes) → §"Sending media".
19
+ - **Reactions** (👀 / ✅ / clear) → §"Send a reaction".
20
+ - **Typing indicator + mark-as-read** → §"Mark as read + typing indicator".
21
+ - **Signature verify (manual)** → §"Webhook signature verification".
22
+ - **Contact resolution → swarm user** → §"Resolve a contact to a swarm user".
23
+ - **Conversation history / message detail / templates list** → §"Read conversation context".
24
+
25
+ If the MCP-tool send returns a 24h-window error (`sessionWindowExpired: true`), fall through to the template path in §"Send a template" — this is exactly what the tool's structured-error points at.
26
+
27
+ ## Setup
28
+
29
+ Swarm config keys (resolve with `get-config key:<NAME> includeSecrets:true` — Lead-only for secrets; workers should ask Lead if they need a value injected):
30
+
31
+ | Key | Value |
32
+ |---|---|
33
+ | `KAPSO_API_BASE_URL` | `https://api.kapso.ai` (host only, no `/platform/v1`) |
34
+ | `KAPSO_API_KEY` | API key (`X-API-Key` header) |
35
+ | `KAPSO_PHONE_NUMBER_ID` | `1035039933036854` — our provisioned number's Meta ID |
36
+ | `KAPSO_WEBHOOK_HMAC_SECRET` | Shared HMAC secret. Kapso signs every webhook request with `X-Webhook-Signature: <hex>` |
37
+
38
+ The Kapso CLI is NOT installed in worker containers. Use direct HTTP or clone the `gokapso/agent-skills` repo for fallback scripts.
39
+
40
+ ```bash
41
+ git clone --depth=1 https://github.com/gokapso/agent-skills /tmp/kapso-skills
42
+ cd /tmp/kapso-skills/skills/integrate-whatsapp && npm i # or observe-whatsapp / automate-whatsapp
43
+ ```
44
+
45
+ The Meta Cloud API is proxied at `$KAPSO_API_BASE_URL/meta/whatsapp/v24.0/...` (auth: `X-API-Key`). Kapso's own platform endpoints live at `$KAPSO_API_BASE_URL/platform/v1/...`.
46
+
47
+ ## Inbound webhook payload (v2)
48
+
49
+ The `kapso-whatsapp-inbound-demo` workflow (`a5700897-8f6b-4e1e-9e6b-dcc10e479cb5`) receives `whatsapp.message.*` and `whatsapp.conversation.*` events at `POST https://api.desplega.agent-swarm.dev/api/webhooks/<workflow-id>`.
50
+
51
+ Shape (top-level keys):
52
+
53
+ ```json
54
+ {
55
+ "message": {
56
+ "id": "wamid.HBgL...", // Meta message id (WAMID)
57
+ "from": "34679077777", // E.164 without +
58
+ "from_user_id": "ES.26772...", // Meta-internal user id
59
+ "timestamp": "1779281573", // unix seconds (string)
60
+ "type": "text", // text | image | audio | video | document | sticker | location | contacts | reaction | ...
61
+ "text": { "body": "ola" }, // only for type=text
62
+ "context": null, // present when the user quote-replied another message
63
+ "kapso": {
64
+ "direction": "inbound|outbound",
65
+ "status": "received|delivered|read|sent|failed",
66
+ "processing_status": "pending|completed",
67
+ "origin": "cloud_api",
68
+ "has_media": false,
69
+ "content": "ola" // text representation (caption / filename / body)
70
+ }
71
+ },
72
+ "conversation": {
73
+ "id": "bd7e888e-...",
74
+ "phone_number": "34679077777",
75
+ "phone_number_id": "1035039933036854",
76
+ "contact_name": "Taras",
77
+ "status": "active",
78
+ "last_active_at": "...",
79
+ "created_at": "...",
80
+ "kapso": {
81
+ "messages_count": 10,
82
+ "last_message_id": "wamid...",
83
+ "last_message_text": "ola",
84
+ "last_inbound_at": "...",
85
+ "last_outbound_at": "..."
86
+ }
87
+ },
88
+ "is_new_conversation": false,
89
+ "phone_number_id": "1035039933036854"
90
+ }
91
+ ```
92
+
93
+ **ALWAYS filter on `message.kapso.direction == "inbound"`** — Kapso fires the webhook for our own outbound sends, deliveries, reads, and failures too. Only inbound events from real humans warrant a task.
94
+
95
+ Test payloads include `"test": true` and `wamid.TEST_...` ids — handle gracefully (treat as a real inbound but mark it test in your reply; do not send a real WhatsApp reply to test payloads).
96
+
97
+ ## Non-text message types
98
+
99
+ `message.type` can be `text`, `image`, `audio`, `video`, `document`, `sticker`, `location`, `contacts`, `reaction`, `button`, or `interactive`. Non-text inbound messages carry a type-specific object:
100
+
101
+ | type | object | key fields |
102
+ |---|---|---|
103
+ | `image` | `message.image` | `id` (media id), `mime_type`, `sha256`, `caption?` |
104
+ | `audio` | `message.audio` | `id`, `mime_type`, `voice` (true = voice note), `sha256` |
105
+ | `video` | `message.video` | `id`, `mime_type`, `sha256`, `caption?` |
106
+ | `document` | `message.document` | `id`, `mime_type`, `filename`, `sha256`, `caption?` |
107
+ | `sticker` | `message.sticker` | `id`, `mime_type`, `animated`, `sha256` |
108
+ | `location` | `message.location` | `latitude`, `longitude`, `name?`, `address?` |
109
+ | `contacts` | `message.contacts[]` | `name`, `phones[]`, `emails[]`, ... |
110
+ | `reaction` | `message.reaction` | `message_id` (wamid being reacted to), `emoji` |
111
+
112
+ `message.kapso.has_media` is `true` for image/audio/video/document/sticker. `message.kapso.content` carries a text representation where one exists (caption, filename). `message.transcript` may be present for audio if Kapso pre-transcribed it.
113
+
114
+ ### Downloading media
115
+
116
+ Media messages carry a Meta **media id** (`message.<type>.id`), not a URL. Two-step download via the Kapso proxy:
117
+
118
+ 1. Resolve the media id to a temporary URL + metadata:
119
+ ```bash
120
+ curl -s -H "X-API-Key: $KAPSO_API_KEY" \
121
+ "$KAPSO_API_BASE_URL/meta/whatsapp/v24.0/<MEDIA_ID>"
122
+ # → { "url": "https://lookaside.fbsbx.com/...", "mime_type": "...", "file_size": ..., "id": "...", "sha256": "..." }
123
+ ```
124
+ 2. Download the binary from that `url` (Meta lookaside URLs expire fast — download immediately):
125
+ ```bash
126
+ curl -sL -H "X-API-Key: $KAPSO_API_KEY" "<url>" -o /tmp/media.bin
127
+ ```
128
+
129
+ NB: verify the exact proxy path against a real media message — the swarm has only received `text` inbound so far. If the lookaside `url` 403s with `X-API-Key`, retry through `$KAPSO_API_BASE_URL/meta/whatsapp/...`.
130
+
131
+ ### Recommended handling per type
132
+
133
+ - **audio / voice notes** → download, then transcribe with ElevenLabs Scribe. Workers should NOT call ElevenLabs directly — the Lead owns audio (see `TOOLS.md`); ask the Lead to transcribe, or escalate. Feed the transcript into the conversation as if it were a text message.
134
+ - **image** → download, then describe / answer with a Claude vision model. A screenshot captioned "debug this" should get a real answer, not "I can't read images".
135
+ - **document** → download; read the text content (PDF/txt/etc.) and act on it.
136
+ - **video** → acknowledge + ask for specifics unless there is a clear transcription need.
137
+ - **location** → use `latitude` / `longitude` directly.
138
+ - **sticker** → treat as a lightweight reaction; usually no substantive reply needed.
139
+ - **reaction** → a user reacting to one of OUR messages. Usually acknowledge silently — do NOT trigger a full reply loop. (The inbound-demo workflow's `debounce` node naturally drops reaction-only events.)
140
+ - **contacts** → extract the shared contact info; act per the conversation.
141
+
142
+ ## Resolve a contact to a swarm user
143
+
144
+ Two paths in order:
145
+
146
+ 1. By name: `resolve-user name:"<contact_name>"` (fuzzy substring match). Returns the canonical profile if there's one. Useful for known team members (Taras / Eze).
147
+ 2. If no match — the contact is unknown. Lead can run `manage-user create name:"<contact_name>" notes:"WhatsApp +<phone>"` to register them. Workers should NOT create users autonomously; ask Lead.
148
+
149
+ Always quote the phone number in `manage-user notes` so future lookups by phone work (until we add a `phone` column to the user registry).
150
+
151
+ ## Read conversation context
152
+
153
+ Use the Kapso platform endpoints via curl (no CLI needed):
154
+
155
+ ```bash
156
+ API_BASE=$(get-config KAPSO_API_BASE_URL) # https://api.kapso.ai
157
+ API_KEY=$(get-config KAPSO_API_KEY)
158
+
159
+ # List conversations for our number
160
+ curl -s -H "X-API-Key: $API_KEY" \
161
+ "$API_BASE/platform/v1/whatsapp/conversations?phone_number_id=1035039933036854&status=active" | jq
162
+
163
+ # Get a single conversation
164
+ curl -s -H "X-API-Key: $API_KEY" \
165
+ "$API_BASE/platform/v1/whatsapp/conversations/<conversation_id>" | jq
166
+
167
+ # List messages for a conversation — USE THE QUERY-PARAM FORM.
168
+ # (The conversation-scoped path /conversations/<id>/messages returns 404.)
169
+ curl -s -H "X-API-Key: $API_KEY" \
170
+ "$API_BASE/platform/v1/whatsapp/messages?conversation_id=<conversation_id>&limit=20" | jq
171
+
172
+ # Single message detail
173
+ curl -s -H "X-API-Key: $API_KEY" \
174
+ "$API_BASE/platform/v1/whatsapp/messages/<wamid>" | jq
175
+ ```
176
+
177
+ The message list is returned newest-first.
178
+
179
+ ## Send a free-form text (within the 24h session window)
180
+
181
+ Per WhatsApp policy, free-form text is only allowed within 24h of the last inbound message. Outside that window you MUST use a pre-approved template (see "Send a template" below).
182
+
183
+ **Common case shortcut:** call the `send-whatsapp-message` MCP tool — it wraps exactly this REST call. The recipe below is the canonical reference and the fallback when you need fields the tool doesn't expose.
184
+
185
+ ```bash
186
+ TO="34679077777"
187
+ TEXT="Hi Taras 👋"
188
+
189
+ curl -s -X POST -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
190
+ -d "{
191
+ \"messaging_product\": \"whatsapp\",
192
+ \"recipient_type\": \"individual\",
193
+ \"to\": \"$TO\",
194
+ \"type\": \"text\",
195
+ \"text\": { \"preview_url\": false, \"body\": \"$TEXT\" }
196
+ }" \
197
+ "$API_BASE/meta/whatsapp/v24.0/1035039933036854/messages" | jq
198
+ ```
199
+
200
+ Returns `{ "messages": [{ "id": "wamid..." }] }` on success. Log the wamid.
201
+
202
+ ### Quote-reply (thread to the original message)
203
+
204
+ Add a `context` object to make the message render as a reply to a specific inbound message. The `reply-whatsapp-message` MCP tool wraps exactly this; use the raw recipe when you need to combine quote-reply with media / templates / reactions (the tool only does text).
205
+
206
+ ```json
207
+ {
208
+ "messaging_product": "whatsapp",
209
+ "recipient_type": "individual",
210
+ "to": "<phone>",
211
+ "context": { "message_id": "<inbound_wamid>" },
212
+ "type": "text",
213
+ "text": { "preview_url": false, "body": "<reply>" }
214
+ }
215
+ ```
216
+
217
+ Prefer quote-replies when answering a specific question — it keeps long conversations legible.
218
+
219
+ ## Sending media (image, document, audio, video)
220
+
221
+ Two-step pipeline through Kapso's Meta proxy: **upload, then send by id**. Sending by `id` is more reliable than `link` (no public-host requirement) — validated 2026-05-20.
222
+
223
+ ### 1. Upload
224
+
225
+ ```bash
226
+ curl -s -X POST -H "X-API-Key: $API_KEY" \
227
+ -F "messaging_product=whatsapp" \
228
+ -F "type=<mime>" \
229
+ -F "file=@/path/to/file.ext;type=<mime>" \
230
+ "$API_BASE/meta/whatsapp/v24.0/1035039933036854/media"
231
+ # → {"id":"<media-id>"}
232
+ ```
233
+
234
+ ### 2. Send by id
235
+
236
+ ```json
237
+ { "type": "image", "image": { "id": "<id>", "caption": "..." } }
238
+ { "type": "document", "document": { "id": "<id>", "filename": "name.ext", "caption": "..." } }
239
+ { "type": "audio", "audio": { "id": "<id>" } }
240
+ { "type": "video", "video": { "id": "<id>", "caption": "..." } }
241
+ ```
242
+
243
+ Quote-reply works on media too — add `"context": { "message_id": "<wamid>" }` at the top level.
244
+
245
+ ### Wide images: pad to ~square, send as image (validated 2026-05-20)
246
+
247
+ WhatsApp scales `type:image` to bubble width + recompresses, so a wide 1200×630 social card renders as a tiny shrunken strip. **The fix is NOT `type:document`** — a `.png` sent as a document shows a plain file card with NO inline preview (must tap+download). Bad UX both ways.
248
+
249
+ Correct approach: **letterbox/pad the wide image onto a ~1:1 (1080×1080) or 4:5 (1080×1350) canvas** with a solid bg fill (white, or a colour sampled from the card's corner), card centered, then send THAT as `type:image`. WhatsApp shows ~1:1–4:5 images large WITH a preview and won't shrink them.
250
+
251
+ Pad with Pillow (ImageMagick is NOT installed in workers; `pip`/`python3 -c` with PIL works):
252
+
253
+ ```python
254
+ from PIL import Image
255
+ src = Image.open("in.png").convert("RGB"); w,h = src.size
256
+ bg = src.getpixel((0,0)) # sample corner for fill
257
+ card = src.resize((1080, round(h*1080/w)), Image.LANCZOS)
258
+ canvas = Image.new("RGB", (1080,1080), bg)
259
+ canvas.paste(card, (0,(1080-card.height)//2)); canvas.save("out.png")
260
+ ```
261
+
262
+ Reserve `type:document` for ACTUAL files — PDFs (which DO render a preview), spreadsheets, etc. — never for images.
263
+
264
+ ### Voice notes (PTT play bar)
265
+
266
+ For PTT (play bar in the bubble), the audio MUST be `audio/ogg` with Opus. MP3 sends as a generic audio attachment, no PTT bar.
267
+ ```bash
268
+ ffmpeg -i in.mp3 -c:a libopus -b:a 32k -application voip out.ogg
269
+ ```
270
+
271
+ ## Mark as read + typing indicator
272
+
273
+ The mark-as-read endpoint doubles as the typing indicator. POST to the same `/messages` endpoint with `status: "read"` and an optional `typing_indicator`:
274
+
275
+ ```bash
276
+ curl -s -X POST -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
277
+ -d '{
278
+ "messaging_product": "whatsapp",
279
+ "status": "read",
280
+ "message_id": "<inbound_wamid>",
281
+ "typing_indicator": { "type": "text" }
282
+ }' \
283
+ "$API_BASE/meta/whatsapp/v24.0/1035039933036854/messages"
284
+ # → {"success":true}
285
+ ```
286
+
287
+ The typing indicator ("typing…" dots) auto-clears after ~25 seconds OR the moment you send any message. For long-running work, re-fire this call every <25s (e.g. right before you POST your reply) to keep the dots visible. Drop the `typing_indicator` field to mark-as-read only.
288
+
289
+ ## Send a reaction
290
+
291
+ ```bash
292
+ curl -s -X POST -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
293
+ -d '{
294
+ "messaging_product": "whatsapp",
295
+ "recipient_type": "individual",
296
+ "to": "<phone>",
297
+ "type": "reaction",
298
+ "reaction": { "message_id": "<wamid>", "emoji": "👀" }
299
+ }' \
300
+ "$API_BASE/meta/whatsapp/v24.0/1035039933036854/messages"
301
+ ```
302
+
303
+ A user can have only ONE reaction per message — sending a new emoji REPLACES the previous one (no explicit remove needed). Send `"emoji": ""` to clear a reaction entirely.
304
+
305
+ ## Send a template (outside the 24h window)
306
+
307
+ If `send-whatsapp-message` returns `sessionWindowExpired: true`, fall through to this path. WhatsApp only allows pre-approved templates outside the 24h customer-service window.
308
+
309
+ ```bash
310
+ curl -s -X POST -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
311
+ -d '{
312
+ "messaging_product": "whatsapp",
313
+ "to": "34679077777",
314
+ "type": "template",
315
+ "template": {
316
+ "name": "<template_name>",
317
+ "language": { "code": "en_US" }
318
+ }
319
+ }' \
320
+ "$API_BASE/meta/whatsapp/v24.0/1035039933036854/messages"
321
+ ```
322
+
323
+ List approved templates first: `GET $API_BASE/platform/v1/whatsapp/templates?phone_number_id=1035039933036854`.
324
+
325
+ ## Webhook signature verification
326
+
327
+ Every Kapso webhook delivery includes `X-Webhook-Signature: <hex>` (HMAC-SHA256 of the raw body using `KAPSO_WEBHOOK_HMAC_SECRET`). The native handler (`/api/integrations/kapso/webhook`, PR #560) and the workflow webhook trigger both verify automatically — the trigger's `hmacHeader` is `X-Webhook-Signature` and `hmacSecret` resolves from swarm config.
328
+
329
+ To verify manually:
330
+
331
+ ```bash
332
+ echo -n "$RAW_BODY" | openssl dgst -sha256 -hmac "$HMAC_SECRET" -hex | awk '{print $2}'
333
+ # Compare hex output with X-Webhook-Signature header (constant-time compare in production).
334
+ ```
335
+
336
+ ## Reply etiquette
337
+
338
+ - Same language as the inbound message (Spanish/English/Catalan — match what they wrote).
339
+ - Brief. WhatsApp is not Slack — 1-3 short messages max.
340
+ - Identify yourself if it's a first interaction in the conversation: "Hi! This is the Desplega agent — Taras wired me up to handle WhatsApp."
341
+ - Quote-reply (`context.message_id`) when answering a specific question.
342
+ - If you can't help (no skill for the request, out of scope) — say so and either escalate to Lead or ask the human to use Slack instead.
343
+ - Always log the outbound wamid in your task output so it's traceable.
344
+
345
+ ## Where this fits in the swarm
346
+
347
+ Two inbound paths exist (PR #560 adds the first, the original demo workflow remains):
348
+
349
+ - **Native handler** (`/api/integrations/kapso/webhook`, PR #560) — fires for any phone number registered via `register-kapso-number`. Verifies HMAC, dedupes by message id (KV `integrations:kapso:dedupe`, 24h TTL), reads the routing mapping from KV (`integrations:kapso:numbers`), and either dispatches a `kapso-inbound` task or delegates to a workflow trigger (advanced override). Also emits a `kapso.message.received` event on the workflow event bus.
350
+ - **Workflow `kapso-whatsapp-inbound-demo`** (`a5700897-8f6b-4e1e-9e6b-dcc10e479cb5`) — fires for unregistered numbers (or numbers whose mapping points at this workflow). Pipeline: `react-eyes` (mark read + typing + 👀) → `debounce` (collapse rapid-fire bursts) → `gate-proceed` → `notify-taras` (agent-task triage) → `finalize` (✅/❌ reaction).
351
+
352
+ **Debounce / batching:** the demo workflow's `debounce` node waits ~8s after each message and only the LAST message of a burst proceeds to the agent task — so a user firing 3 quick messages produces ONE task, not three. The agent is told the `batchSize` and should read trailing history and answer the whole burst in one reply. When >1 messages are collapsed, the user sees a "🧵 Got your N messages" note.
353
+
354
+ The agent-task triages like any other interaction; route heavier work to specialists via `send-task` (always include the WhatsApp source context so they can reply back).
355
+
356
+ HMAC verification is enforced (signed mode) on both paths.
357
+
358
+ ## Common gotchas
359
+
360
+ - Phone numbers from Kapso are E.164 **without `+`** (e.g. `34679077777`). Add `+` when displaying to humans, drop it when calling the API.
361
+ - `message.text.body` is only present for `type:"text"`. For other types read `message.<type>` (see the table above) or `message.kapso.content` for a text representation.
362
+ - Outbound status events (`delivered`, `read`) are NOT a customer interaction — skip them. Filter by `message.kapso.direction == "inbound"`.
363
+ - Real inbound messages commonly arrive with `status: "delivered"` (delivered to us). Do NOT skip on status — only `direction` signals inbound vs outbound.
364
+ - Kapso sometimes sends test payloads with `"test": true` and `wamid.TEST_*` ids. Don't reply to test payloads — just complete the task with a note.
365
+ - The provisioned phone number id (`1035039933036854`) is **our** number, not the recipient's. The recipient is in `message.from` / `conversation.phone_number`.
366
+ - The message-list endpoint is `/platform/v1/whatsapp/messages?conversation_id=X` — the conversation-scoped `/conversations/<id>/messages` path 404s.
367
+ - **Wide images shrink — pad them, don't send as document.** `type:image` scales to bubble width; a wide social card becomes a strip. Sending it as `type:document` removes the preview entirely. Fix: pad onto a ~1:1/4:5 canvas and send as `type:image`. See "Sending media".
368
+ - **MCP tools cover text-only.** `send-whatsapp-message` and `reply-whatsapp-message` are deliberately thin — templates / media / reactions / typing / mark-as-read are NOT in the tool surface. For those, use the REST recipes in this skill directly.
369
+
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "skill",
3
+ "name": "kv-storage",
4
+ "displayName": "KV Storage",
5
+ "slug": "kv-storage",
6
+ "title": "KV Storage",
7
+ "description": "Use swarm key-value storage for small state, idempotency keys, and resumable automation.",
8
+ "version": "1.0.0",
9
+ "category": "skills",
10
+ "placeholders": [],
11
+ "runAllSeedersCandidate": true,
12
+ "tags": ["state", "automation", "idempotency"]
13
+ }
@@ -0,0 +1,111 @@
1
+ # KV Storage
2
+
3
+ The agent-swarm provides a key-value store for small durable state that needs to persist across tasks, sessions, and container restarts. It's backed by the swarm API and accessible via the `kv-get`, `kv-set`, `kv-delete`, `kv-incr`, and `kv-list` MCP tools.
4
+
5
+ ## When to Use KV Storage
6
+
7
+ - Deduplication records (e.g., "have I already processed this Slack message?")
8
+ - Idempotency keys (e.g., "did I already send this email today?")
9
+ - Cross-session state for scheduled tasks (e.g., "last processed PR number")
10
+ - Routing/mapping tables (e.g., "which agent handles phone number X?")
11
+ - Small counters and accumulators
12
+
13
+ Do NOT use KV for:
14
+ - Secrets or credentials (use swarm config with `isSecret=true`)
15
+ - Large artifacts (use agent-fs)
16
+ - Logs or audit trails (use agent-fs or `store-progress`)
17
+
18
+ ## Core Operations
19
+
20
+ ```bash
21
+ # Set a key
22
+ kv-set key="my:key:path" value='{"lastRunAt":"2026-05-28","count":5}' ttl=86400
23
+
24
+ # Get a key
25
+ kv-get key="my:key:path"
26
+
27
+ # Delete a key
28
+ kv-delete key="my:key:path"
29
+
30
+ # Increment a counter (atomic)
31
+ kv-incr key="my:counter" by=1
32
+
33
+ # List keys with a prefix
34
+ kv-list prefix="my:"
35
+ ```
36
+
37
+ ## Key Naming Conventions
38
+
39
+ Use namespaced keys tied to the task, schedule, workflow, or external object. Good patterns:
40
+
41
+ ```
42
+ # Deduplication
43
+ integrations:slack:dedupe:<message-ts>
44
+ integrations:kapso:dedupe:<wamid>
45
+
46
+ # Routing/mapping
47
+ integrations:kapso:numbers:<phone-number-id>
48
+
49
+ # Schedule state
50
+ schedules:<schedule-id>:last-run-at
51
+ schedules:<schedule-id>:last-sha
52
+
53
+ # Workflow state
54
+ workflows:<workflow-id>:run:<run-id>:step-state
55
+
56
+ # Agent-specific
57
+ agent:<agent-id>:cache:<topic>
58
+ ```
59
+
60
+ ## TTLs for Temporary State
61
+
62
+ Set TTLs on deduplication records and temporary state to avoid unbounded key accumulation. Common TTL values:
63
+
64
+ - Deduplication windows: `86400` (24h) or `3600` (1h)
65
+ - Session state: `86400` (24h)
66
+ - Short-lived flags: `300–3600` (5 min–1h)
67
+
68
+ Omit TTL for persistent mappings (routing tables, last-known-good state).
69
+
70
+ ## Value Format
71
+
72
+ Store compact JSON objects, not large blobs. Include:
73
+ - Timestamps for idempotency records
74
+ - Source identifiers to track provenance
75
+ - Version or hash for cache invalidation
76
+
77
+ ```json
78
+ {
79
+ "processedAt": "2026-05-28T10:00:00Z",
80
+ "taskId": "173ca713-...",
81
+ "source": "slack:C0AR967K0KZ:1748430000.000000"
82
+ }
83
+ ```
84
+
85
+ ## Atomicity
86
+
87
+ `kv-incr` is atomic — safe for counters shared across concurrent tasks. `kv-set` is last-write-wins. For state that multiple tasks might write concurrently, use `kv-incr` with a sequence number or include a task ID in the value to detect conflicts.
88
+
89
+ ## Example: Deduplication
90
+
91
+ ```javascript
92
+ // Before processing a message
93
+ const dedupKey = `integrations:slack:dedupe:${message.ts}`;
94
+ const existing = await kvGet(dedupKey);
95
+ if (existing) {
96
+ // Already processed — skip
97
+ return;
98
+ }
99
+
100
+ // Process the message
101
+ await processMessage(message);
102
+
103
+ // Mark as processed with 24h TTL
104
+ await kvSet(dedupKey, JSON.stringify({ processedAt: new Date().toISOString() }), { ttl: 86400 });
105
+ ```
106
+
107
+ ## Trade-offs
108
+
109
+ **KV vs agent-fs:** KV is fast and API-native (no file I/O), but values are opaque blobs — no search, no versioning, no human browsing. Use KV for machine-consumed state; use agent-fs for human-reviewable artifacts and documents.
110
+
111
+ **KV vs swarm config:** swarm config is for operator-configured values (API keys, account IDs, flags). KV is for dynamic runtime state that tasks write and read themselves.
@@ -0,0 +1,20 @@
1
+ {
2
+ "kind": "skill",
3
+ "name": "linear-interaction",
4
+ "displayName": "Linear Interaction",
5
+ "slug": "linear-interaction",
6
+ "title": "Linear Interaction",
7
+ "description": "Create issues, update status, and post comments in Linear with explicit project placeholders.",
8
+ "version": "1.0.0",
9
+ "category": "skills",
10
+ "placeholders": [
11
+ "LINEAR_PROJECT_ID"
12
+ ],
13
+ "runAllSeedersCandidate": false,
14
+ "tags": [
15
+ "linear",
16
+ "tracker",
17
+ "project-management"
18
+ ],
19
+ "must": true
20
+ }