@codespar/cli 0.17.0 → 0.18.1

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 (482) hide show
  1. package/README.md +1 -0
  2. package/dist/surface.js +53 -1
  3. package/dist/surface.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/bills-agent/README.md +1 -1
  6. package/templates/bills-agent/agents/bills-agent/AGENTS.md +1 -1
  7. package/templates/bills-agent/agents/bills-agent/CLAUDE.md +1 -1
  8. package/templates/bills-agent/agents/bills-agent/README.md +6 -5
  9. package/templates/bills-agent/agents/bills-agent/src/modules/embedded-consent.ts +5 -4
  10. package/templates/bills-agent/packages/agent-core/README.md +14 -1
  11. package/templates/bills-agent/packages/agent-core/package.json +1 -1
  12. package/templates/bills-agent/packages/agent-core/src/agent.ts +39 -1
  13. package/templates/bills-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  14. package/templates/bills-agent/packages/agent-core/src/api/client.ts +28 -5
  15. package/templates/bills-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  16. package/templates/bills-agent/packages/agent-core/src/api/rail.ts +62 -26
  17. package/templates/bills-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  18. package/templates/bills-agent/packages/agent-core/src/approval.ts +14 -2
  19. package/templates/bills-agent/packages/agent-core/src/bundle.ts +35 -3
  20. package/templates/bills-agent/packages/agent-core/src/channels.ts +49 -3
  21. package/templates/bills-agent/packages/agent-core/src/check.ts +25 -2
  22. package/templates/bills-agent/packages/agent-core/src/engine.ts +235 -46
  23. package/templates/bills-agent/packages/agent-core/src/hash.ts +57 -1
  24. package/templates/bills-agent/packages/agent-core/src/index.ts +2 -0
  25. package/templates/bills-agent/packages/agent-core/src/quote.ts +50 -0
  26. package/templates/bills-agent/packages/agent-core/src/rail.ts +89 -4
  27. package/templates/bills-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  28. package/templates/bills-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  29. package/templates/bills-agent/packages/agent-core/src/revocation.ts +1 -1
  30. package/templates/bills-agent/packages/agent-core/src/state-machine.ts +10 -1
  31. package/templates/bills-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  32. package/templates/bills-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  33. package/templates/bills-agent/packages/agent-core/src/tools.ts +17 -6
  34. package/templates/bills-agent/packages/agent-core/src/types.ts +54 -1
  35. package/templates/bills-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  36. package/templates/bills-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  37. package/templates/bills-agent/packages/agent-core/test/composition.test.ts +204 -0
  38. package/templates/bills-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  39. package/templates/bills-agent/packages/agent-core/test/helpers.ts +4 -1
  40. package/templates/bills-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  41. package/templates/bills-agent/packages/agent-core/test/quote.test.ts +198 -0
  42. package/templates/bills-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  43. package/templates/bills-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  44. package/templates/bills-agent/packages/agent-core/test/receivables.test.ts +103 -3
  45. package/templates/bills-agent/packages/agent-runtime/README.md +12 -0
  46. package/templates/bills-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  47. package/templates/bills-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  48. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  49. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  50. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  51. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  52. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  53. package/templates/bills-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  54. package/templates/bills-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  55. package/templates/bills-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  56. package/templates/bills-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  57. package/templates/bills-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  58. package/templates/bills-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  59. package/templates/bills-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  60. package/templates/bills-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  61. package/templates/bills-agent/packages/agent-runtime/src/index.ts +1 -0
  62. package/templates/bills-agent/packages/agent-runtime/src/inspect.ts +5 -1
  63. package/templates/bills-agent/packages/agent-runtime/src/kit.ts +25 -2
  64. package/templates/bills-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  65. package/templates/bills-agent/packages/agent-runtime/src/setup.ts +13 -3
  66. package/templates/bills-agent/packages/agent-runtime/src/terminal.ts +127 -18
  67. package/templates/bills-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  68. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  69. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  70. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  71. package/templates/checkout-agent/LICENSE +21 -0
  72. package/templates/checkout-agent/README.md +28 -0
  73. package/templates/checkout-agent/_gitignore +10 -0
  74. package/templates/checkout-agent/agents/checkout-agent/.env.example +5 -0
  75. package/templates/checkout-agent/agents/checkout-agent/AGENTS.md +20 -0
  76. package/templates/checkout-agent/agents/checkout-agent/CLAUDE.md +20 -0
  77. package/templates/checkout-agent/agents/checkout-agent/README.md +94 -0
  78. package/templates/checkout-agent/agents/checkout-agent/SYSTEM_PROMPT.md +36 -0
  79. package/templates/checkout-agent/agents/checkout-agent/agent.yaml +30 -0
  80. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/README.md +28 -0
  81. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-beatriz.json +11 -0
  82. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina-retomada.json +10 -0
  83. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/pedido-marina.json +11 -0
  84. package/templates/checkout-agent/agents/checkout-agent/channels/whatsapp/templates.json +49 -0
  85. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap-human.json +12 -0
  86. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.json +11 -0
  87. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/beneficiary-swap.transcript.jsonl +5 -0
  88. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.json +13 -0
  89. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/coupon-unknown.transcript.jsonl +4 -0
  90. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.json +26 -0
  91. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/exfiltration.transcript.jsonl +3 -0
  92. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.json +11 -0
  93. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/false-authority.transcript.jsonl +5 -0
  94. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.json +12 -0
  95. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/fractioning.transcript.jsonl +5 -0
  96. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.json +13 -0
  97. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/model-total.transcript.jsonl +5 -0
  98. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.json +15 -0
  99. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/payment-claimed.transcript.jsonl +7 -0
  100. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.json +12 -0
  101. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/price-injected.transcript.jsonl +5 -0
  102. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.json +13 -0
  103. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/prompt-injection.transcript.jsonl +4 -0
  104. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.json +16 -0
  105. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped-human.transcript.jsonl +9 -0
  106. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.json +15 -0
  107. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/quantity-swapped.transcript.jsonl +8 -0
  108. package/templates/checkout-agent/agents/checkout-agent/evals/adversarial/webhook-replay.json +11 -0
  109. package/templates/checkout-agent/agents/checkout-agent/evals/eval.yaml +11 -0
  110. package/templates/checkout-agent/agents/checkout-agent/guardrails.json +25 -0
  111. package/templates/checkout-agent/agents/checkout-agent/mandate.example.json +28 -0
  112. package/templates/checkout-agent/agents/checkout-agent/package.json +33 -0
  113. package/templates/checkout-agent/agents/checkout-agent/runbook.md +19 -0
  114. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.json +42 -0
  115. package/templates/checkout-agent/agents/checkout-agent/scenarios/beneficiary-not-allowed.transcript.jsonl +4 -0
  116. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.json +32 -0
  117. package/templates/checkout-agent/agents/checkout-agent/scenarios/cap-exceeded.transcript.jsonl +4 -0
  118. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.json +61 -0
  119. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-recomposed.transcript.jsonl +9 -0
  120. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.json +61 -0
  121. package/templates/checkout-agent/agents/checkout-agent/scenarios/cart-replaced.transcript.jsonl +9 -0
  122. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.json +36 -0
  123. package/templates/checkout-agent/agents/checkout-agent/scenarios/charge-expired.transcript.jsonl +6 -0
  124. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.json +23 -0
  125. package/templates/checkout-agent/agents/checkout-agent/scenarios/coupon-unknown.transcript.jsonl +4 -0
  126. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.json +72 -0
  127. package/templates/checkout-agent/agents/checkout-agent/scenarios/escalated-above-threshold.transcript.jsonl +9 -0
  128. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.json +15 -0
  129. package/templates/checkout-agent/agents/checkout-agent/scenarios/happy-path.transcript.jsonl +6 -0
  130. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.json +23 -0
  131. package/templates/checkout-agent/agents/checkout-agent/scenarios/item-unavailable.transcript.jsonl +3 -0
  132. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.json +42 -0
  133. package/templates/checkout-agent/agents/checkout-agent/scenarios/mandate-revoked.transcript.jsonl +12 -0
  134. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.json +14 -0
  135. package/templates/checkout-agent/agents/checkout-agent/scenarios/nfse-failed.transcript.jsonl +9 -0
  136. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.json +34 -0
  137. package/templates/checkout-agent/agents/checkout-agent/scenarios/payment-claimed.transcript.jsonl +7 -0
  138. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.json +33 -0
  139. package/templates/checkout-agent/agents/checkout-agent/scenarios/price-injected.transcript.jsonl +4 -0
  140. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.json +32 -0
  141. package/templates/checkout-agent/agents/checkout-agent/scenarios/prompt-injection.transcript.jsonl +4 -0
  142. package/templates/checkout-agent/agents/checkout-agent/src/catalog.ts +92 -0
  143. package/templates/checkout-agent/agents/checkout-agent/src/kit.ts +338 -0
  144. package/templates/checkout-agent/agents/checkout-agent/src/modules/bolepix-receivables.ts +179 -0
  145. package/templates/checkout-agent/agents/checkout-agent/src/modules/nfse-invoice.ts +374 -0
  146. package/templates/checkout-agent/agents/checkout-agent/src/modules/storefront-cart.ts +220 -0
  147. package/templates/checkout-agent/agents/checkout-agent/src/pricing.ts +297 -0
  148. package/templates/checkout-agent/agents/checkout-agent/test/adversarial.test.ts +117 -0
  149. package/templates/checkout-agent/agents/checkout-agent/test/check.test.ts +67 -0
  150. package/templates/checkout-agent/agents/checkout-agent/test/cli.test.ts +169 -0
  151. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/order-marina.transcript.jsonl +5 -0
  152. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/ordered-marina-awaiting-payer.transcript.jsonl +6 -0
  153. package/templates/checkout-agent/agents/checkout-agent/test/fixtures/tap-falar-agora-marina.transcript.jsonl +2 -0
  154. package/templates/checkout-agent/agents/checkout-agent/test/nfse.test.ts +170 -0
  155. package/templates/checkout-agent/agents/checkout-agent/test/pricing.test.ts +138 -0
  156. package/templates/checkout-agent/agents/checkout-agent/test/scenarios.test.ts +113 -0
  157. package/templates/checkout-agent/agents/checkout-agent/test/whatsapp.test.ts +171 -0
  158. package/templates/checkout-agent/agents/checkout-agent/tools.json +68 -0
  159. package/templates/checkout-agent/agents/checkout-agent/tsconfig.json +10 -0
  160. package/templates/checkout-agent/package.json +38 -0
  161. package/templates/checkout-agent/packages/agent-core/README.md +34 -0
  162. package/templates/checkout-agent/packages/agent-core/package.json +24 -0
  163. package/templates/checkout-agent/packages/agent-core/src/agent.ts +163 -0
  164. package/templates/checkout-agent/packages/agent-core/src/api/charge-rail.ts +195 -0
  165. package/templates/checkout-agent/packages/agent-core/src/api/client.ts +76 -0
  166. package/templates/checkout-agent/packages/agent-core/src/api/mandate-status.ts +74 -0
  167. package/templates/checkout-agent/packages/agent-core/src/api/rail.ts +153 -0
  168. package/templates/checkout-agent/packages/agent-core/src/api/sandbox-payer.ts +43 -0
  169. package/templates/checkout-agent/packages/agent-core/src/approval.ts +135 -0
  170. package/templates/checkout-agent/packages/agent-core/src/bundle.ts +191 -0
  171. package/templates/checkout-agent/packages/agent-core/src/channels.ts +186 -0
  172. package/templates/checkout-agent/packages/agent-core/src/check.ts +276 -0
  173. package/templates/checkout-agent/packages/agent-core/src/clock.ts +42 -0
  174. package/templates/checkout-agent/packages/agent-core/src/engine.ts +1074 -0
  175. package/templates/checkout-agent/packages/agent-core/src/escalate.ts +85 -0
  176. package/templates/checkout-agent/packages/agent-core/src/events.ts +40 -0
  177. package/templates/checkout-agent/packages/agent-core/src/guardrails.ts +45 -0
  178. package/templates/checkout-agent/packages/agent-core/src/hash.ts +118 -0
  179. package/templates/checkout-agent/packages/agent-core/src/ids.ts +10 -0
  180. package/templates/checkout-agent/packages/agent-core/src/index.ts +34 -0
  181. package/templates/checkout-agent/packages/agent-core/src/mandate.ts +100 -0
  182. package/templates/checkout-agent/packages/agent-core/src/manifest.ts +76 -0
  183. package/templates/checkout-agent/packages/agent-core/src/providers/anthropic.ts +76 -0
  184. package/templates/checkout-agent/packages/agent-core/src/providers/replay.ts +63 -0
  185. package/templates/checkout-agent/packages/agent-core/src/providers/types.ts +39 -0
  186. package/templates/checkout-agent/packages/agent-core/src/quote.ts +50 -0
  187. package/templates/checkout-agent/packages/agent-core/src/rail.ts +186 -0
  188. package/templates/checkout-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  189. package/templates/checkout-agent/packages/agent-core/src/receipt-verification.ts +577 -0
  190. package/templates/checkout-agent/packages/agent-core/src/revocation.ts +30 -0
  191. package/templates/checkout-agent/packages/agent-core/src/secrets.ts +30 -0
  192. package/templates/checkout-agent/packages/agent-core/src/state/store.ts +307 -0
  193. package/templates/checkout-agent/packages/agent-core/src/state-machine.ts +157 -0
  194. package/templates/checkout-agent/packages/agent-core/src/stubs/charge-rail.ts +195 -0
  195. package/templates/checkout-agent/packages/agent-core/src/stubs/mandate-status.ts +58 -0
  196. package/templates/checkout-agent/packages/agent-core/src/stubs/rail.ts +209 -0
  197. package/templates/checkout-agent/packages/agent-core/src/tools.ts +66 -0
  198. package/templates/checkout-agent/packages/agent-core/src/types.ts +187 -0
  199. package/templates/checkout-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  200. package/templates/checkout-agent/packages/agent-core/test/approval.test.ts +117 -0
  201. package/templates/checkout-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  202. package/templates/checkout-agent/packages/agent-core/test/bundle.test.ts +85 -0
  203. package/templates/checkout-agent/packages/agent-core/test/clock.test.ts +25 -0
  204. package/templates/checkout-agent/packages/agent-core/test/composition.test.ts +204 -0
  205. package/templates/checkout-agent/packages/agent-core/test/engine.test.ts +596 -0
  206. package/templates/checkout-agent/packages/agent-core/test/escalate.test.ts +42 -0
  207. package/templates/checkout-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  208. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt-keys.json +18 -0
  209. package/templates/checkout-agent/packages/agent-core/test/fixtures/staging-receipt.json +24 -0
  210. package/templates/checkout-agent/packages/agent-core/test/helpers.ts +142 -0
  211. package/templates/checkout-agent/packages/agent-core/test/mandate-status.test.ts +229 -0
  212. package/templates/checkout-agent/packages/agent-core/test/manifest.test.ts +45 -0
  213. package/templates/checkout-agent/packages/agent-core/test/quote.test.ts +198 -0
  214. package/templates/checkout-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  215. package/templates/checkout-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  216. package/templates/checkout-agent/packages/agent-core/test/receipt-verification.test.ts +316 -0
  217. package/templates/checkout-agent/packages/agent-core/test/receivables.test.ts +416 -0
  218. package/templates/checkout-agent/packages/agent-core/test/secrets.test.ts +28 -0
  219. package/templates/checkout-agent/packages/agent-core/test/state-machine.test.ts +67 -0
  220. package/templates/checkout-agent/packages/agent-core/test/transitions.type-test.ts +59 -0
  221. package/templates/checkout-agent/packages/agent-core/tsconfig.json +10 -0
  222. package/templates/checkout-agent/packages/agent-runtime/README.md +80 -0
  223. package/templates/checkout-agent/packages/agent-runtime/bin.mjs +32 -0
  224. package/templates/checkout-agent/packages/agent-runtime/package.json +23 -0
  225. package/templates/checkout-agent/packages/agent-runtime/src/adversarial.ts +199 -0
  226. package/templates/checkout-agent/packages/agent-runtime/src/agent.ts +68 -0
  227. package/templates/checkout-agent/packages/agent-runtime/src/channels/contact.ts +13 -0
  228. package/templates/checkout-agent/packages/agent-runtime/src/channels/index.ts +72 -0
  229. package/templates/checkout-agent/packages/agent-runtime/src/channels/rules.ts +120 -0
  230. package/templates/checkout-agent/packages/agent-runtime/src/channels/types.ts +173 -0
  231. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +459 -0
  232. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +216 -0
  233. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/evidence.ts +109 -0
  234. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +350 -0
  235. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +180 -0
  236. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/present.ts +47 -0
  237. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +120 -0
  238. package/templates/checkout-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +127 -0
  239. package/templates/checkout-agent/packages/agent-runtime/src/cli.ts +98 -0
  240. package/templates/checkout-agent/packages/agent-runtime/src/commands/check.ts +15 -0
  241. package/templates/checkout-agent/packages/agent-runtime/src/commands/decide.ts +63 -0
  242. package/templates/checkout-agent/packages/agent-runtime/src/commands/eval.ts +58 -0
  243. package/templates/checkout-agent/packages/agent-runtime/src/commands/inspect.ts +100 -0
  244. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +367 -0
  245. package/templates/checkout-agent/packages/agent-runtime/src/commands/poll.ts +145 -0
  246. package/templates/checkout-agent/packages/agent-runtime/src/commands/reconcile.ts +42 -0
  247. package/templates/checkout-agent/packages/agent-runtime/src/commands/rerun.ts +86 -0
  248. package/templates/checkout-agent/packages/agent-runtime/src/commands/resume.ts +49 -0
  249. package/templates/checkout-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +134 -0
  250. package/templates/checkout-agent/packages/agent-runtime/src/commands/start.ts +248 -0
  251. package/templates/checkout-agent/packages/agent-runtime/src/commands/verify.ts +330 -0
  252. package/templates/checkout-agent/packages/agent-runtime/src/commands/webhook.ts +40 -0
  253. package/templates/checkout-agent/packages/agent-runtime/src/default-kit.ts +83 -0
  254. package/templates/checkout-agent/packages/agent-runtime/src/index.ts +13 -0
  255. package/templates/checkout-agent/packages/agent-runtime/src/inspect.ts +801 -0
  256. package/templates/checkout-agent/packages/agent-runtime/src/kit.ts +229 -0
  257. package/templates/checkout-agent/packages/agent-runtime/src/poll.ts +105 -0
  258. package/templates/checkout-agent/packages/agent-runtime/src/scenarios.ts +274 -0
  259. package/templates/checkout-agent/packages/agent-runtime/src/setup.ts +239 -0
  260. package/templates/checkout-agent/packages/agent-runtime/src/terminal.ts +263 -0
  261. package/templates/checkout-agent/packages/agent-runtime/src/webhook.ts +131 -0
  262. package/templates/checkout-agent/packages/agent-runtime/test/inspect.test.ts +336 -0
  263. package/templates/checkout-agent/packages/agent-runtime/test/verify-cli.test.ts +329 -0
  264. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +277 -0
  265. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-evidence.test.ts +94 -0
  266. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-rules.test.ts +174 -0
  267. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-session.test.ts +167 -0
  268. package/templates/checkout-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  269. package/templates/checkout-agent/packages/agent-runtime/tsconfig.json +9 -0
  270. package/templates/checkout-agent/scripts/whatsapp-emulator.mjs +84 -0
  271. package/templates/checkout-agent/tsconfig.base.json +16 -0
  272. package/templates/checkout-agent/vitest.config.ts +12 -0
  273. package/templates/collections-agent/README.md +6 -1
  274. package/templates/collections-agent/agents/collections-agent/README.md +12 -8
  275. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/README.md +8 -0
  276. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/acordo-1042-retomada.json +10 -0
  277. package/templates/collections-agent/agents/collections-agent/channels/whatsapp/templates.json +27 -1
  278. package/templates/collections-agent/agents/collections-agent/src/kit.ts +2 -0
  279. package/templates/collections-agent/agents/collections-agent/src/modules/bolepix-receivables.ts +12 -2
  280. package/templates/collections-agent/agents/collections-agent/test/ambiguous-charge.test.ts +55 -0
  281. package/templates/collections-agent/agents/collections-agent/test/fixtures/tap-emitir-nova-1042.transcript.jsonl +2 -0
  282. package/templates/collections-agent/agents/collections-agent/test/whatsapp-poll.test.ts +12 -6
  283. package/templates/collections-agent/agents/collections-agent/test/whatsapp-status-replies.test.ts +148 -0
  284. package/templates/collections-agent/agents/collections-agent/test/whatsapp.test.ts +5 -0
  285. package/templates/collections-agent/package.json +1 -0
  286. package/templates/collections-agent/packages/agent-core/README.md +14 -1
  287. package/templates/collections-agent/packages/agent-core/package.json +1 -1
  288. package/templates/collections-agent/packages/agent-core/src/agent.ts +39 -1
  289. package/templates/collections-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  290. package/templates/collections-agent/packages/agent-core/src/api/client.ts +28 -5
  291. package/templates/collections-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  292. package/templates/collections-agent/packages/agent-core/src/api/rail.ts +62 -26
  293. package/templates/collections-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  294. package/templates/collections-agent/packages/agent-core/src/approval.ts +14 -2
  295. package/templates/collections-agent/packages/agent-core/src/bundle.ts +35 -3
  296. package/templates/collections-agent/packages/agent-core/src/channels.ts +49 -3
  297. package/templates/collections-agent/packages/agent-core/src/check.ts +25 -2
  298. package/templates/collections-agent/packages/agent-core/src/engine.ts +235 -46
  299. package/templates/collections-agent/packages/agent-core/src/hash.ts +57 -1
  300. package/templates/collections-agent/packages/agent-core/src/index.ts +2 -0
  301. package/templates/collections-agent/packages/agent-core/src/quote.ts +50 -0
  302. package/templates/collections-agent/packages/agent-core/src/rail.ts +89 -4
  303. package/templates/collections-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  304. package/templates/collections-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  305. package/templates/collections-agent/packages/agent-core/src/revocation.ts +1 -1
  306. package/templates/collections-agent/packages/agent-core/src/state-machine.ts +10 -1
  307. package/templates/collections-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  308. package/templates/collections-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  309. package/templates/collections-agent/packages/agent-core/src/tools.ts +17 -6
  310. package/templates/collections-agent/packages/agent-core/src/types.ts +54 -1
  311. package/templates/collections-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  312. package/templates/collections-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  313. package/templates/collections-agent/packages/agent-core/test/composition.test.ts +204 -0
  314. package/templates/collections-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  315. package/templates/collections-agent/packages/agent-core/test/helpers.ts +4 -1
  316. package/templates/collections-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  317. package/templates/collections-agent/packages/agent-core/test/quote.test.ts +198 -0
  318. package/templates/collections-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  319. package/templates/collections-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  320. package/templates/collections-agent/packages/agent-core/test/receivables.test.ts +103 -3
  321. package/templates/collections-agent/packages/agent-runtime/README.md +12 -0
  322. package/templates/collections-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  323. package/templates/collections-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  324. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  325. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  326. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  327. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  328. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  329. package/templates/collections-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  330. package/templates/collections-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  331. package/templates/collections-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  332. package/templates/collections-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  333. package/templates/collections-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  334. package/templates/collections-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  335. package/templates/collections-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  336. package/templates/collections-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  337. package/templates/collections-agent/packages/agent-runtime/src/index.ts +1 -0
  338. package/templates/collections-agent/packages/agent-runtime/src/inspect.ts +5 -1
  339. package/templates/collections-agent/packages/agent-runtime/src/kit.ts +25 -2
  340. package/templates/collections-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  341. package/templates/collections-agent/packages/agent-runtime/src/setup.ts +13 -3
  342. package/templates/collections-agent/packages/agent-runtime/src/terminal.ts +127 -18
  343. package/templates/collections-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  344. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  345. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  346. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  347. package/templates/collections-agent/scripts/whatsapp-emulator.mjs +84 -0
  348. package/templates/hello-agent/README.md +1 -1
  349. package/templates/hello-agent/packages/agent-core/README.md +14 -1
  350. package/templates/hello-agent/packages/agent-core/package.json +1 -1
  351. package/templates/hello-agent/packages/agent-core/src/agent.ts +39 -1
  352. package/templates/hello-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  353. package/templates/hello-agent/packages/agent-core/src/api/client.ts +28 -5
  354. package/templates/hello-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  355. package/templates/hello-agent/packages/agent-core/src/api/rail.ts +62 -26
  356. package/templates/hello-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  357. package/templates/hello-agent/packages/agent-core/src/approval.ts +14 -2
  358. package/templates/hello-agent/packages/agent-core/src/bundle.ts +35 -3
  359. package/templates/hello-agent/packages/agent-core/src/channels.ts +49 -3
  360. package/templates/hello-agent/packages/agent-core/src/check.ts +25 -2
  361. package/templates/hello-agent/packages/agent-core/src/engine.ts +235 -46
  362. package/templates/hello-agent/packages/agent-core/src/hash.ts +57 -1
  363. package/templates/hello-agent/packages/agent-core/src/index.ts +2 -0
  364. package/templates/hello-agent/packages/agent-core/src/quote.ts +50 -0
  365. package/templates/hello-agent/packages/agent-core/src/rail.ts +89 -4
  366. package/templates/hello-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  367. package/templates/hello-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  368. package/templates/hello-agent/packages/agent-core/src/revocation.ts +1 -1
  369. package/templates/hello-agent/packages/agent-core/src/state-machine.ts +10 -1
  370. package/templates/hello-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  371. package/templates/hello-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  372. package/templates/hello-agent/packages/agent-core/src/tools.ts +17 -6
  373. package/templates/hello-agent/packages/agent-core/src/types.ts +54 -1
  374. package/templates/hello-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  375. package/templates/hello-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  376. package/templates/hello-agent/packages/agent-core/test/composition.test.ts +204 -0
  377. package/templates/hello-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  378. package/templates/hello-agent/packages/agent-core/test/helpers.ts +4 -1
  379. package/templates/hello-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  380. package/templates/hello-agent/packages/agent-core/test/quote.test.ts +198 -0
  381. package/templates/hello-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  382. package/templates/hello-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  383. package/templates/hello-agent/packages/agent-core/test/receivables.test.ts +103 -3
  384. package/templates/hello-agent/packages/agent-runtime/README.md +12 -0
  385. package/templates/hello-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  386. package/templates/hello-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  387. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  388. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  389. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  390. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  391. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  392. package/templates/hello-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  393. package/templates/hello-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  394. package/templates/hello-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  395. package/templates/hello-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  396. package/templates/hello-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  397. package/templates/hello-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  398. package/templates/hello-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  399. package/templates/hello-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  400. package/templates/hello-agent/packages/agent-runtime/src/index.ts +1 -0
  401. package/templates/hello-agent/packages/agent-runtime/src/inspect.ts +5 -1
  402. package/templates/hello-agent/packages/agent-runtime/src/kit.ts +25 -2
  403. package/templates/hello-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  404. package/templates/hello-agent/packages/agent-runtime/src/setup.ts +13 -3
  405. package/templates/hello-agent/packages/agent-runtime/src/terminal.ts +127 -18
  406. package/templates/hello-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  407. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  408. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  409. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  410. package/templates/kits.lock.json +29 -6
  411. package/templates/supplier-payments-agent/README.md +1 -1
  412. package/templates/supplier-payments-agent/agents/supplier-payments-agent/AGENTS.md +1 -1
  413. package/templates/supplier-payments-agent/agents/supplier-payments-agent/CLAUDE.md +1 -1
  414. package/templates/supplier-payments-agent/agents/supplier-payments-agent/README.md +11 -9
  415. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/kit.ts +1 -1
  416. package/templates/supplier-payments-agent/agents/supplier-payments-agent/src/modules/batch-payout.ts +67 -20
  417. package/templates/supplier-payments-agent/agents/supplier-payments-agent/test/batch.test.ts +504 -5
  418. package/templates/supplier-payments-agent/packages/agent-core/README.md +14 -1
  419. package/templates/supplier-payments-agent/packages/agent-core/package.json +1 -1
  420. package/templates/supplier-payments-agent/packages/agent-core/src/agent.ts +39 -1
  421. package/templates/supplier-payments-agent/packages/agent-core/src/api/charge-rail.ts +54 -34
  422. package/templates/supplier-payments-agent/packages/agent-core/src/api/client.ts +28 -5
  423. package/templates/supplier-payments-agent/packages/agent-core/src/api/mandate-status.ts +28 -12
  424. package/templates/supplier-payments-agent/packages/agent-core/src/api/rail.ts +62 -26
  425. package/templates/supplier-payments-agent/packages/agent-core/src/api/sandbox-payer.ts +3 -18
  426. package/templates/supplier-payments-agent/packages/agent-core/src/approval.ts +14 -2
  427. package/templates/supplier-payments-agent/packages/agent-core/src/bundle.ts +35 -3
  428. package/templates/supplier-payments-agent/packages/agent-core/src/channels.ts +49 -3
  429. package/templates/supplier-payments-agent/packages/agent-core/src/check.ts +25 -2
  430. package/templates/supplier-payments-agent/packages/agent-core/src/engine.ts +235 -46
  431. package/templates/supplier-payments-agent/packages/agent-core/src/hash.ts +57 -1
  432. package/templates/supplier-payments-agent/packages/agent-core/src/index.ts +2 -0
  433. package/templates/supplier-payments-agent/packages/agent-core/src/quote.ts +50 -0
  434. package/templates/supplier-payments-agent/packages/agent-core/src/rail.ts +89 -4
  435. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-chain.ts +270 -0
  436. package/templates/supplier-payments-agent/packages/agent-core/src/receipt-verification.ts +194 -18
  437. package/templates/supplier-payments-agent/packages/agent-core/src/revocation.ts +1 -1
  438. package/templates/supplier-payments-agent/packages/agent-core/src/state-machine.ts +10 -1
  439. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/mandate-status.ts +4 -3
  440. package/templates/supplier-payments-agent/packages/agent-core/src/stubs/rail.ts +100 -32
  441. package/templates/supplier-payments-agent/packages/agent-core/src/tools.ts +17 -6
  442. package/templates/supplier-payments-agent/packages/agent-core/src/types.ts +54 -1
  443. package/templates/supplier-payments-agent/packages/agent-core/test/approval-seal.test.ts +233 -0
  444. package/templates/supplier-payments-agent/packages/agent-core/test/attempt-replay.test.ts +265 -0
  445. package/templates/supplier-payments-agent/packages/agent-core/test/composition.test.ts +204 -0
  446. package/templates/supplier-payments-agent/packages/agent-core/test/fixtures/production-receipt-keys.json +1 -0
  447. package/templates/supplier-payments-agent/packages/agent-core/test/helpers.ts +4 -1
  448. package/templates/supplier-payments-agent/packages/agent-core/test/mandate-status.test.ts +85 -9
  449. package/templates/supplier-payments-agent/packages/agent-core/test/quote.test.ts +198 -0
  450. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-chain.test.ts +220 -0
  451. package/templates/supplier-payments-agent/packages/agent-core/test/receipt-read-verification.test.ts +197 -0
  452. package/templates/supplier-payments-agent/packages/agent-core/test/receivables.test.ts +103 -3
  453. package/templates/supplier-payments-agent/packages/agent-runtime/README.md +12 -0
  454. package/templates/supplier-payments-agent/packages/agent-runtime/src/adversarial.ts +32 -2
  455. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/types.ts +53 -4
  456. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/cloud-api.ts +123 -22
  457. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/emulator.ts +30 -20
  458. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/index.ts +146 -21
  459. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/open.ts +40 -3
  460. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/run.ts +8 -1
  461. package/templates/supplier-payments-agent/packages/agent-runtime/src/channels/whatsapp/session.ts +16 -2
  462. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll-whatsapp.ts +70 -20
  463. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/poll.ts +2 -1
  464. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/resume.ts +8 -2
  465. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start-whatsapp.ts +3 -0
  466. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/start.ts +6 -3
  467. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/verify.ts +239 -38
  468. package/templates/supplier-payments-agent/packages/agent-runtime/src/commands/webhook.ts +3 -2
  469. package/templates/supplier-payments-agent/packages/agent-runtime/src/index.ts +1 -0
  470. package/templates/supplier-payments-agent/packages/agent-runtime/src/inspect.ts +5 -1
  471. package/templates/supplier-payments-agent/packages/agent-runtime/src/kit.ts +25 -2
  472. package/templates/supplier-payments-agent/packages/agent-runtime/src/scenarios.ts +8 -2
  473. package/templates/supplier-payments-agent/packages/agent-runtime/src/setup.ts +13 -3
  474. package/templates/supplier-payments-agent/packages/agent-runtime/src/terminal.ts +127 -18
  475. package/templates/supplier-payments-agent/packages/agent-runtime/test/verify-cli.test.ts +211 -9
  476. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-cloud-api.test.ts +45 -0
  477. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-session.test.ts +32 -0
  478. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-status-replies.test.ts +182 -0
  479. package/templates/bills-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  480. package/templates/collections-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  481. package/templates/hello-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
  482. package/templates/supplier-payments-agent/packages/agent-runtime/test/whatsapp-emulator.integration.test.ts +0 -204
@@ -13,9 +13,9 @@
13
13
  * caller and have no path to this object at all — not a check, a shape: there
14
14
  * is no method here that an operator line could be handed to.
15
15
  */
16
- import type { ProofBundle, WhatsAppTemplate } from "@codespar/agent-core";
16
+ import { declaredReplies, type ProofBundle, type QuickReply, type WhatsAppTemplate } from "@codespar/agent-core";
17
17
  import { checkOutbound, type HoursRule, type RuleContext } from "../rules.js";
18
- import type { Channel, ChannelBackend, ChannelLogLine, Conversation, InboundMessage, OutboundBody, SentMessage } from "../types.js";
18
+ import type { Channel, ChannelBackend, ChannelLogLine, Conversation, DeliveryState, InboundMessage, OutboundBody, SentMessage, StatusUpdate } from "../types.js";
19
19
  import { maskContact } from "../contact.js";
20
20
  import { SessionWindow, type SessionState } from "./session.js";
21
21
 
@@ -44,6 +44,14 @@ export interface WhatsAppChannelOptions {
44
44
  * something in front of the person — that is `send`, and it is the only one.
45
45
  */
46
46
  render?: ((line: string) => void) | undefined;
47
+ /**
48
+ * The provider reported that a message the agent sent was NOT delivered.
49
+ * Called with the status line, which names the outcome the message told
50
+ * when it told one — that outcome was not told, whatever the send answered.
51
+ */
52
+ onDeliveryFailed?: ((line: ChannelLogLine) => void) | undefined;
53
+ /** Lines an earlier run wrote for this conversation, so a status about a message it sent can be tied back to what that message was. */
54
+ priorLines?: ReadonlyArray<ChannelLogLine> | undefined;
47
55
  }
48
56
 
49
57
  export class WhatsAppChannel implements Channel {
@@ -52,10 +60,21 @@ export class WhatsAppChannel implements Channel {
52
60
  private readonly session: SessionWindow;
53
61
  private readonly lines: ChannelLogLine[] = [];
54
62
  private lastInbound: InboundMessage | undefined;
63
+ private readonly replies: Map<string, QuickReply>;
64
+ /** The latest state the provider reported per message id, this process. */
65
+ private readonly delivery = new Map<string, DeliveryState>();
66
+ /**
67
+ * Failures reported for a message not yet in the log. A provider can post
68
+ * the status before the send that minted the id has answered (the emulator
69
+ * does, synchronously); the failure is acted on once the message is logged
70
+ * and it is known what the message was.
71
+ */
72
+ private readonly earlyFailures = new Map<string, ChannelLogLine>();
55
73
 
56
74
  constructor(private readonly options: WhatsAppChannelOptions) {
57
75
  this.conversation = options.conversation;
58
76
  this.session = new SessionWindow(options.templates ?? [], options.session);
77
+ this.replies = declaredReplies(options.templates ?? []);
59
78
  }
60
79
 
61
80
  get backend(): string {
@@ -87,33 +106,125 @@ export class WhatsAppChannel implements Channel {
87
106
  return this.session.remainingSeconds(this.options.now());
88
107
  }
89
108
 
109
+ /** The registry's fallback template, the one sent for an outcome the kit has no copy for. */
110
+ fallbackTemplate(): string | undefined {
111
+ return (this.options.templates ?? []).find((t) => t.fallback)?.name;
112
+ }
113
+
90
114
  /** What the agent declared about a template it is about to send. The language is the registry's, never the sender's guess. */
91
115
  declaredTemplate(name: string): WhatsAppTemplate | undefined {
92
116
  return this.session.declared(name);
93
117
  }
94
118
 
95
119
  async open(): Promise<void> {
120
+ this.options.backend.onStatus?.((status) => this.observeStatus(status));
96
121
  await this.options.backend.open();
97
122
  }
98
123
 
124
+ /**
125
+ * The person's next turn. A typed message is the turn. A TAPPED quick reply
126
+ * is a turn only when its id is one a declared template offers, and then the
127
+ * turn is that reply's declared intent, never the button's title and never
128
+ * anything the model makes of it. A tap on an id nobody declared — or on a
129
+ * message this conversation did not send, when the provider names the
130
+ * message — is recorded, reported to the operator and skipped: a button the
131
+ * agent never offered has no meaning to hand on.
132
+ */
99
133
  async next(): Promise<InboundMessage | undefined> {
100
- const message = await this.options.backend.next();
101
- if (!message) return undefined;
102
- this.lastInbound = message;
103
- this.session.observeInbound(message.timestamp);
104
- this.record({
134
+ for (;;) {
135
+ const message = await this.options.backend.next();
136
+ if (!message) return undefined;
137
+ // Any inbound, tap or text, reopens the 24-hour window: the person wrote.
138
+ this.lastInbound = message;
139
+ this.session.observeInbound(message.timestamp);
140
+ const base = {
141
+ at: this.options.now().toISOString(),
142
+ direction: "in" as const,
143
+ contact: maskContact(message.from),
144
+ message_id: message.id,
145
+ state: "delivered" as const,
146
+ // The provider's clock, which is the only one the 24-hour window is
147
+ // counted on. A later run reads the window back from here.
148
+ provider_timestamp: message.timestamp,
149
+ };
150
+ if (!message.reply) {
151
+ this.record({ ...base, kind: "text", text: message.text });
152
+ return message;
153
+ }
154
+ const tapped = { type: message.reply.type, id: message.reply.id, title: message.reply.title };
155
+ const declared = this.replies.get(message.reply.id);
156
+ const refusal = !declared
157
+ ? { rule: "reply_not_offered", detail: `the person tapped ${message.reply.id}, which no template this agent declares offers; it is not a turn` }
158
+ : message.reply.context_id && !this.offeredOn(message.reply.context_id, message.reply.id)
159
+ ? { rule: "reply_not_offered", detail: `the tap names message ${message.reply.context_id}, which this conversation did not send with ${message.reply.id}` }
160
+ : undefined;
161
+ if (refusal) {
162
+ this.record({ ...base, kind: "reply", reply: tapped, refused: refusal });
163
+ this.options.say?.(` [whatsapp] toque ignorado (${refusal.rule}): ${refusal.detail}`);
164
+ continue;
165
+ }
166
+ this.record({ ...base, kind: "reply", reply: tapped, text: declared!.intent });
167
+ return { ...message, text: declared!.intent };
168
+ }
169
+ }
170
+
171
+ /** Whether the message the provider named is one this conversation sent carrying that reply. */
172
+ private offeredOn(messageId: string, replyId: string): boolean {
173
+ return [...(this.options.priorLines ?? []), ...this.lines].some((l) => l.direction === "out" && l.message_id === messageId && (l.offered ?? []).includes(replyId));
174
+ }
175
+
176
+ /**
177
+ * A status webhook. Recorded in the conversation; a `failed` is also the
178
+ * operator's business and, when the message told an outcome, the outcome's:
179
+ * the provider just said the person never got it. A `read` is recorded and
180
+ * nothing more — it says a device displayed the message, which is not
181
+ * consent, not acknowledgement of a debt or an order, and not a reply.
182
+ */
183
+ private observeStatus(status: StatusUpdate): void {
184
+ const sent = [...(this.options.priorLines ?? []), ...this.lines].find((l) => l.direction === "out" && l.message_id === status.message_id);
185
+ this.delivery.set(status.message_id, status.status);
186
+ const line: ChannelLogLine = {
105
187
  at: this.options.now().toISOString(),
106
- direction: "in",
107
- contact: maskContact(message.from),
108
- kind: "text",
109
- message_id: message.id,
110
- state: "delivered",
111
- text: message.text,
112
- // The provider's clock, which is the only one the 24-hour window is
113
- // counted on. A later run reads the window back from here.
114
- provider_timestamp: message.timestamp,
115
- });
116
- return message;
188
+ direction: "status",
189
+ contact: maskContact(this.conversation.contact),
190
+ kind: "status",
191
+ message_id: status.message_id,
192
+ state: status.status,
193
+ provider_timestamp: status.timestamp,
194
+ ...(sent?.about ? { about: sent.about } : {}),
195
+ ...(status.errors.length ? { errors: status.errors } : {}),
196
+ };
197
+ this.record(line);
198
+ if (status.status !== "failed") return;
199
+ if (!sent) {
200
+ this.earlyFailures.set(status.message_id, line);
201
+ return;
202
+ }
203
+ this.reportFailure(line);
204
+ }
205
+
206
+ private reportFailure(line: ChannelLogLine): void {
207
+ const status = { message_id: line.message_id, errors: line.errors ?? [] };
208
+ const codes = status.errors.map((e) => `${e.code ?? "?"}${e.details ? ` ${e.details}` : ""}`).join("; ") || "no error given";
209
+ this.options.say?.(
210
+ ` [operador] ENTREGA FALHOU da mensagem ${status.message_id} (${codes})${line.about ? ` — era o aviso de ${line.about.execution_id} (${line.about.state}): a pessoa NAO foi avisada` : ""}`,
211
+ );
212
+ this.options.onDeliveryFailed?.(line);
213
+ }
214
+
215
+ /** The latest status the provider reported for a message this process saw a status for. */
216
+ deliveryOf(messageId: string): DeliveryState | undefined {
217
+ return this.delivery.get(messageId);
218
+ }
219
+
220
+ /**
221
+ * Waits for status webhooks that may still be in flight. A provider reports
222
+ * a failure after the send answered, and a process that exits first never
223
+ * hears it; this is the short, bounded wait a caller that has just told an
224
+ * outcome gives the provider before it reports the outcome as told.
225
+ */
226
+ async settle(ms: number): Promise<void> {
227
+ if (ms > 0) await new Promise((resolve) => setTimeout(resolve, ms));
117
228
  }
118
229
 
119
230
  async send(body: OutboundBody): Promise<SentMessage> {
@@ -134,9 +245,13 @@ export class WhatsAppChannel implements Channel {
134
245
  return sent;
135
246
  }
136
247
 
137
- const sent = await this.options.backend.deliver(to, body);
248
+ // A template carries the quick replies its declaration offers, and only those.
249
+ const buttons = body.kind === "template" ? this.session.declared(body.template)?.buttons : undefined;
250
+ const outgoing: OutboundBody = body.kind === "template" && buttons?.length ? { ...body, buttons: buttons.map((b) => ({ id: b.id, title: b.title })) } : body;
251
+ const sent = await this.options.backend.deliver(to, outgoing);
138
252
  if (sent.refused) this.options.say?.(` [whatsapp] recusado pelo backend (${sent.refused.rule}): ${sent.refused.detail}`);
139
- this.logOutbound(body, sent);
253
+ if (sent.refused?.rule === "session_window_closed") this.session.observeProviderShut();
254
+ this.logOutbound(outgoing, sent);
140
255
  return sent;
141
256
  }
142
257
 
@@ -154,6 +269,12 @@ export class WhatsAppChannel implements Channel {
154
269
  }
155
270
 
156
271
  private logOutbound(body: OutboundBody, sent: SentMessage): void {
272
+ const early = sent.id ? this.earlyFailures.get(sent.id) : undefined;
273
+ if (early) {
274
+ this.earlyFailures.delete(sent.id);
275
+ const about = (body.kind === "text" || body.kind === "template") && body.about ? body.about : undefined;
276
+ queueMicrotask(() => this.reportFailure({ ...early, ...(about ? { about } : {}) }));
277
+ }
157
278
  this.record({
158
279
  at: this.options.now().toISOString(),
159
280
  direction: "out",
@@ -163,6 +284,8 @@ export class WhatsAppChannel implements Channel {
163
284
  state: sent.state,
164
285
  ...(textOf(body) !== undefined ? { text: textOf(body)! } : {}),
165
286
  ...(sent.refused ? { refused: sent.refused } : {}),
287
+ ...((body.kind === "text" || body.kind === "template") && body.about ? { about: body.about } : {}),
288
+ ...(body.kind === "template" && body.buttons?.length ? { offered: body.buttons.map((b) => b.id) } : {}),
166
289
  });
167
290
  }
168
291
 
@@ -193,6 +316,8 @@ export class WhatsAppChannel implements Channel {
193
316
 
194
317
  /** One console line for one message. The refusals are visible: a message nobody got is part of the conversation's story. */
195
318
  function draw(line: ChannelLogLine): string {
319
+ if (line.direction === "status") return `(status ${line.message_id}: ${line.state}${line.errors?.length ? ` ${line.errors.map((e) => e.code).join(",")}` : ""})`;
320
+ if (line.kind === "reply") return `${line.contact}: [toque: ${line.reply?.title ?? "?"}]${line.refused ? ` (ignorado: ${line.refused.rule})` : ` -> ${line.text ?? ""}`}`;
196
321
  const who = line.direction === "in" ? `${line.contact}:` : "loja:";
197
322
  const what =
198
323
  line.kind === "instrument"
@@ -214,7 +339,7 @@ function textOf(body: OutboundBody): string | undefined {
214
339
  case "media":
215
340
  return body.caption;
216
341
  case "template":
217
- return `[template ${body.template}] ${body.variables.join(" | ")}`;
342
+ return `[template ${body.template}] ${body.variables.join(" | ")}${body.buttons?.length ? ` [${body.buttons.map((b) => b.title).join("] [")}]` : ""}`;
218
343
  }
219
344
  }
220
345
 
@@ -15,9 +15,9 @@ import type { ConversationScript, ProofBundle } from "@codespar/agent-core";
15
15
  import type { Agent } from "../../agent.js";
16
16
  import type { Setup } from "../../setup.js";
17
17
  import { knownSubjects, loadTemplates } from "../index.js";
18
- import type { ChannelBackend, Conversation } from "../types.js";
18
+ import type { ChannelBackend, ChannelLogLine, Conversation } from "../types.js";
19
19
  import { WhatsAppChannel } from "./index.js";
20
- import { toGraphNumber, WhatsAppCloudApi, loadCloudApiConfig, type CloudApiConfig } from "./cloud-api.js";
20
+ import { WhatsAppCloudApi, loadCloudApiConfig, type CloudApiConfig } from "./cloud-api.js";
21
21
  import { EmulatorDriver, EMULATOR_DEFAULTS, EMULATOR_ENV, WhatsAppEmulator } from "./emulator.js";
22
22
  import type { SessionState } from "./session.js";
23
23
 
@@ -52,6 +52,41 @@ export interface OpenWhatsAppOptions {
52
52
  session?: SessionState | undefined;
53
53
  /** Draws the conversation on the operator's console. */
54
54
  render?: ((line: string) => void) | undefined;
55
+ /** What earlier runs wrote for this conversation: a status about one of their messages is tied back to it. */
56
+ priorLines?: ReadonlyArray<ChannelLogLine> | undefined;
57
+ }
58
+
59
+ /**
60
+ * How long a command that has just told an outcome waits for status webhooks
61
+ * before it reports the outcome as told. A provider reports a failed delivery
62
+ * AFTER the send answered, and a process that has exited hears nothing; this
63
+ * bounds the wait. `WHATSAPP_STATUS_GRACE_MS` overrides it (0 turns it off).
64
+ */
65
+ export function statusGraceMs(env: NodeJS.ProcessEnv = process.env): number {
66
+ const raw = Number(env["WHATSAPP_STATUS_GRACE_MS"]);
67
+ return Number.isFinite(raw) && raw >= 0 && env["WHATSAPP_STATUS_GRACE_MS"]?.trim() ? raw : 1000;
68
+ }
69
+
70
+ /** The cursor that says an outcome's message came back `failed`, so a later poll reports it as not told rather than as told. */
71
+ export function undeliveredCursor(executionId: string, state: string): string {
72
+ return `undelivered:${executionId}:${state}`;
73
+ }
74
+
75
+ /**
76
+ * A `failed` status is an event in the proof bundle, always. When the message
77
+ * told an outcome it is `message.debtor.failed`, and the cursor above is taken:
78
+ * the cursor that says the person was told (`markTold`) stays held — a failed
79
+ * delivery is not retried by itself, because the usual cause (not a WhatsApp
80
+ * number, blocked) fails again — but nothing reports that outcome as told.
81
+ */
82
+ export function recordDeliveryFailure(s: Setup, line: ChannelLogLine): void {
83
+ const payload = { message_id: line.message_id, errors: line.errors ?? [], channel: "whatsapp" };
84
+ if (!line.about) {
85
+ s.engine.note("message.failed", null, payload);
86
+ return;
87
+ }
88
+ s.store.setCursor(undeliveredCursor(line.about.execution_id, line.about.state), JSON.stringify(payload));
89
+ s.engine.note("message.debtor.failed", line.about.execution_id, { ...payload, state: line.about.state });
55
90
  }
56
91
 
57
92
  export interface OpenedWhatsApp {
@@ -102,7 +137,7 @@ export function buildWhatsApp(options: OpenWhatsAppOptions): OpenedWhatsApp | {
102
137
  webhookPort: Number(env[EMULATOR_ENV.webhookPort]?.trim() || EMULATOR_DEFAULTS.webhookPort),
103
138
  };
104
139
  driver = new EmulatorDriver(url);
105
- sessionKey = `${config.phoneNumberId}:${toGraphNumber(options.conversation.contact)}`;
140
+ sessionKey = `${config.phoneNumberId}:${options.conversation.contact}`;
106
141
  backend = new WhatsAppEmulator({
107
142
  config,
108
143
  conversation: { contact: options.conversation.contact },
@@ -129,6 +164,8 @@ export function buildWhatsApp(options: OpenWhatsAppOptions): OpenedWhatsApp | {
129
164
  say,
130
165
  render: options.render ?? say,
131
166
  ...(options.session ? { session: options.session } : {}),
167
+ ...(options.priorLines ? { priorLines: options.priorLines } : {}),
168
+ onDeliveryFailed: (line) => recordDeliveryFailure(s, line),
132
169
  });
133
170
 
134
171
  return { channel, ...(driver ? { driver } : {}), ...(sessionKey ? { sessionKey } : {}) };
@@ -20,6 +20,7 @@ import { handleExecution, type TerminalOptions } from "../../terminal.js";
20
20
  import type { Setup } from "../../setup.js";
21
21
  import type { OutboundBody } from "../types.js";
22
22
  import { instrumentBodies } from "./present.js";
23
+ import { statusGraceMs } from "./open.js";
23
24
  import type { WhatsAppChannel } from "./index.js";
24
25
 
25
26
  export interface ConverseOptions {
@@ -63,6 +64,7 @@ class Outbox {
63
64
  export async function converse(options: ConverseOptions): Promise<ConverseResult> {
64
65
  const { setup: s, channel, approver, say } = options;
65
66
  const outbox = new Outbox(channel);
67
+ let toldOutcome = false;
66
68
  const replies: string[] = [];
67
69
  const toolCalls: Array<{ name: string; refused: boolean }> = [];
68
70
  let turns = 0;
@@ -77,7 +79,10 @@ export async function converse(options: ConverseOptions): Promise<ConverseResult
77
79
  setup: s,
78
80
  approver,
79
81
  say,
80
- tell: (line: string) => outbox.push({ kind: "text", text: line }),
82
+ tell: (line: string, about?: Execution) => {
83
+ if (about) toldOutcome = true;
84
+ outbox.push({ kind: "text", text: line, ...(about ? { about: { execution_id: about.id, state: about.state } } : {}) });
85
+ },
81
86
  presentInstrument,
82
87
  ...(options.decision ? { decision: options.decision } : {}),
83
88
  ...(options.ask ? { ask: options.ask } : {}),
@@ -105,6 +110,8 @@ export async function converse(options: ConverseOptions): Promise<ConverseResult
105
110
  replies.push(result.reply);
106
111
  }
107
112
  await outbox.drain();
113
+ // An outcome told in this run may still come back failed; give the provider its bounded moment before the receiver closes.
114
+ if (toldOutcome) await channel.settle(statusGraceMs());
108
115
  } finally {
109
116
  await channel.close();
110
117
  }
@@ -33,6 +33,8 @@ export type TemplateRefusal = { rule: string; detail: string } | undefined;
33
33
  export class SessionWindow {
34
34
  private readonly templates: Map<string, WhatsAppTemplate>;
35
35
  private lastInboundAt: number | undefined;
36
+ /** The provider said the window is shut (131047) after ours read it open. Holds until the person writes again. */
37
+ private shutByProvider = false;
36
38
 
37
39
  constructor(templates: readonly WhatsAppTemplate[], initial?: SessionState) {
38
40
  this.templates = new Map(templates.map((t) => [t.name, t]));
@@ -40,12 +42,24 @@ export class SessionWindow {
40
42
  }
41
43
 
42
44
  observeInbound(timestamp: number): void {
43
- if (this.lastInboundAt === undefined || timestamp > this.lastInboundAt) this.lastInboundAt = timestamp;
45
+ if (this.lastInboundAt === undefined || timestamp > this.lastInboundAt) {
46
+ this.lastInboundAt = timestamp;
47
+ this.shutByProvider = false;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * The provider refused a free-form message because its window is shut. Its
53
+ * clock is the one the 24 hours are counted on, so from here the window
54
+ * reads shut whatever ours says, and the next message is a template.
55
+ */
56
+ observeProviderShut(): void {
57
+ this.shutByProvider = true;
44
58
  }
45
59
 
46
60
  /** Seconds left before free-form messages stop being allowed; 0 when the window is shut or was never opened. */
47
61
  remainingSeconds(now: Date): number {
48
- if (this.lastInboundAt === undefined) return 0;
62
+ if (this.lastInboundAt === undefined || this.shutByProvider) return 0;
49
63
  return Math.max(0, this.lastInboundAt + SESSION_WINDOW_SECONDS - Math.floor(now.getTime() / 1000));
50
64
  }
51
65
 
@@ -32,14 +32,14 @@ import { relative } from "node:path";
32
32
  import { ProofBundle, type ChargeInstrument, type Execution } from "@codespar/agent-core";
33
33
  import type { Agent } from "../agent.js";
34
34
  import { runsDir, type Setup } from "../setup.js";
35
- import { waitForPayer } from "../terminal.js";
35
+ import { followUp, waitForPayer } from "../terminal.js";
36
36
  import { resolveConversation } from "../channels/index.js";
37
- import { buildWhatsApp, simulatedCost, type WhatsAppBackendName } from "../channels/whatsapp/open.js";
37
+ import { buildWhatsApp, simulatedCost, statusGraceMs, undeliveredCursor, type WhatsAppBackendName } from "../channels/whatsapp/open.js";
38
38
  import { EmulatorUnreachableError } from "../channels/whatsapp/emulator.js";
39
39
  import { sessionStateFromChannelLog, type SessionState } from "../channels/whatsapp/session.js";
40
40
  import { instrumentBodies } from "../channels/whatsapp/present.js";
41
41
  import type { WhatsAppChannel } from "../channels/whatsapp/index.js";
42
- import type { SentMessage } from "../channels/types.js";
42
+ import type { ChannelLogLine, OutboundBody, SentMessage } from "../channels/types.js";
43
43
 
44
44
  export interface PollWhatsAppOptions {
45
45
  agent: Agent;
@@ -57,8 +57,8 @@ export interface PollWhatsAppOptions {
57
57
 
58
58
  /** How the outcome reached the person, or why it did not. */
59
59
  type Delivery =
60
- | { told: true; carrier: "text" | "template"; template?: string }
61
- | { told: false; reason: "already_told" | "still_open" | "no_template_for_outcome" | "refused"; detail?: string };
60
+ | { told: true; carrier: "text" | "template"; template?: string; fallback?: true }
61
+ | { told: false; reason: "already_told" | "still_open" | "no_template_for_outcome" | "refused" | "delivery_failed"; detail?: string };
62
62
 
63
63
  interface Polled {
64
64
  id: string;
@@ -112,7 +112,7 @@ export async function pollWhatsApp(options: PollWhatsAppOptions): Promise<number
112
112
  // window is counted from the person's last message on the PROVIDER's clock,
113
113
  // which only that record holds, and the confirmation is appended THERE, so
114
114
  // one conversation stays one record instead of ending in a second folder.
115
- const { bundle, session } = conversationRecord(agent, mine);
115
+ const { bundle, session, lines: priorLines } = conversationRecord(agent, mine);
116
116
  if (!bundle) {
117
117
  say(`nenhum bundle encontrado para ${mine.map((e) => e.run_id).join(", ")}: a conversa nao pode ser retomada sem o registro dela`);
118
118
  return 1;
@@ -129,6 +129,7 @@ export async function pollWhatsApp(options: PollWhatsAppOptions): Promise<number
129
129
  // Nobody writes in a poll: the person already said what they had to say,
130
130
  // and this run is here because the PAYER acted, not because the person did.
131
131
  session,
132
+ priorLines,
132
133
  });
133
134
  if ("refusal" in built) {
134
135
  for (const line of built.refusal) say(line);
@@ -176,6 +177,7 @@ export async function pollWhatsApp(options: PollWhatsAppOptions): Promise<number
176
177
  const closed = result.execution;
177
178
  if (closed.state !== "executing") await s.engine.collectReceipts(closed.id);
178
179
  const delivery = await tellOutcome(channel, s, closed);
180
+ await followUp(closed, s, say);
179
181
  results.push({
180
182
  id: closed.id,
181
183
  state: closed.state,
@@ -188,7 +190,7 @@ export async function pollWhatsApp(options: PollWhatsAppOptions): Promise<number
188
190
  });
189
191
  say(
190
192
  `${closed.id}: ${closed.state}${closed.reason ? ` (${closed.reason})` : ""} apos ${result.rounds} consulta(s), ${result.seconds}s — ${
191
- delivery.told ? `avisado por ${delivery.carrier === "template" ? `template ${delivery.template}` : "mensagem livre"}` : `nao avisado (${delivery.reason}${delivery.detail ? `: ${delivery.detail}` : ""})`
193
+ delivery.told ? `avisado por ${delivery.carrier === "template" ? `template ${delivery.template}${delivery.fallback ? " (reserva: o kit nao tem copia para este desfecho)" : ""}` : "mensagem livre"}` : `nao avisado (${delivery.reason}${delivery.detail ? `: ${delivery.detail}` : ""})`
192
194
  }`,
193
195
  );
194
196
  }
@@ -219,6 +221,7 @@ export async function pollWhatsApp(options: PollWhatsAppOptions): Promise<number
219
221
  // not know it. The other two are not failures — `already_told` is this
220
222
  // command running twice, which is the thing it is built to be safe under,
221
223
  // and `still_open` is a wait that ran out, which the 3 above already said.
224
+ // `delivery_failed` is the provider saying the person never got it, and is the same failure as a refusal.
222
225
  return results.some((r) => !r.delivery.told && r.delivery.reason !== "already_told" && r.delivery.reason !== "still_open") ? 1 : 0;
223
226
  }
224
227
 
@@ -229,11 +232,12 @@ export async function pollWhatsApp(options: PollWhatsAppOptions): Promise<number
229
232
  * itself is read across all of them, because the person's last message is the
230
233
  * person's last message whichever run recorded it.
231
234
  */
232
- function conversationRecord(agent: Agent, executions: readonly Execution[]): { bundle: ProofBundle | undefined; session: SessionState } {
235
+ function conversationRecord(agent: Agent, executions: readonly Execution[]): { bundle: ProofBundle | undefined; session: SessionState; lines: ChannelLogLine[] } {
233
236
  const runs = runsDir(agent);
234
237
  const seen = new Set<string>();
235
238
  let bundle: ProofBundle | undefined;
236
239
  let lastInboundAt: number | undefined;
240
+ const all: ChannelLogLine[] = [];
237
241
  // `list()` orders by creation, so the last execution that carries a log is the most recent record of the conversation.
238
242
  for (const execution of executions) {
239
243
  if (seen.has(execution.run_id)) continue;
@@ -242,11 +246,12 @@ function conversationRecord(agent: Agent, executions: readonly Execution[]): { b
242
246
  if (!opened) continue;
243
247
  const lines = opened.readChannel();
244
248
  if (!lines.length) continue;
249
+ all.push(...(lines as unknown as ChannelLogLine[]));
245
250
  bundle = opened;
246
251
  const state = sessionStateFromChannelLog(lines);
247
252
  if (state.lastInboundAt !== undefined && (lastInboundAt === undefined || state.lastInboundAt > lastInboundAt)) lastInboundAt = state.lastInboundAt;
248
253
  }
249
- return { bundle, session: { lastInboundAt } };
254
+ return { bundle, session: { lastInboundAt }, lines: all };
250
255
  }
251
256
 
252
257
  /**
@@ -287,31 +292,76 @@ async function tellOutcome(channel: WhatsAppChannel, s: Setup, execution: Execut
287
292
  // last-one-wins, so a kit that says two sentences does not lose one.
288
293
  const lines: string[] = [];
289
294
  const told = s.kit.announceOutcome?.(execution, s, (line) => void lines.push(line)) ?? false;
290
- if (!told) return { told: false, reason: "already_told" };
291
- const sent = await channel.send({ kind: "text", text: lines.join("\n") });
292
- return deliveryOf(sent, "text");
295
+ if (!told) return toldBefore(s, execution);
296
+ const sent = await channel.send({ kind: "text", text: lines.join("\n"), about: aboutOf(execution) });
297
+ // Our clock read the window open and the provider's read it shut (131047).
298
+ // The provider's is the one the window is counted on, so the message goes
299
+ // the way it would have gone had we known: as the template. The cursor is
300
+ // already this call's, taken by `announceOutcome` above.
301
+ if (sent.refused?.rule !== "session_window_closed") return confirmed(channel, sent, deliveryOf(sent, "text"));
302
+ return tellByTemplate(channel, s, execution, { cursorHeld: true });
293
303
  }
304
+ return tellByTemplate(channel, s, execution, { cursorHeld: false });
305
+ }
306
+
307
+ function aboutOf(execution: Execution): NonNullable<Extract<OutboundBody, { kind: "text" }>["about"]> {
308
+ return { execution_id: execution.id, state: execution.state };
309
+ }
294
310
 
295
- const chosen = s.kit.outcomeTemplate?.(execution);
311
+ /** The cursor was taken by an earlier run. Told, unless the provider said since that the message never arrived. */
312
+ function toldBefore(s: Setup, execution: Execution): Delivery {
313
+ const failed = s.store.getCursor(undeliveredCursor(execution.id, execution.state));
314
+ return failed ? { told: false, reason: "delivery_failed", detail: `an earlier message came back failed: ${failed}` } : { told: false, reason: "already_told" };
315
+ }
316
+
317
+ /**
318
+ * A send that answered is not yet a message that arrived. The provider may
319
+ * report a failure a moment later; the channel records it (and the bundle,
320
+ * and the operator hear of it), and this gives it the bounded wait before
321
+ * saying the person was told.
322
+ */
323
+ async function confirmed(channel: WhatsAppChannel, sent: SentMessage, delivery: Delivery): Promise<Delivery> {
324
+ if (!delivery.told || !sent.id) return delivery;
325
+ await channel.settle(statusGraceMs());
326
+ if (channel.deliveryOf(sent.id) !== "failed") return delivery;
327
+ const line = channel.log().find((l: ChannelLogLine) => l.direction === "status" && l.message_id === sent.id && l.state === "failed");
328
+ return { told: false, reason: "delivery_failed", detail: `the provider reported ${sent.id} failed${line?.errors?.length ? ` (${line.errors.map((e) => e.code).join(", ")})` : ""}` };
329
+ }
330
+
331
+ /**
332
+ * The outcome as a template. The kit's own copy when it has one; the
333
+ * registry's FALLBACK when it has none — a template that says there is news
334
+ * and asks the person to answer, and states nothing about the outcome, so it
335
+ * cannot be the wrong news (`npm run check` requires exactly one, taking no
336
+ * variables). A kit that names a template the registry does not declare is a
337
+ * defect, reported as one and not papered over by the fallback.
338
+ */
339
+ async function tellByTemplate(channel: WhatsAppChannel, s: Setup, execution: Execution, options: { cursorHeld: boolean }): Promise<Delivery> {
340
+ const chosen: { template: string; variables: string[]; fallback?: true } | undefined = s.kit.outcomeTemplate?.(execution) ?? fallbackOf(channel);
296
341
  if (!chosen) {
297
342
  return {
298
343
  told: false,
299
344
  reason: "no_template_for_outcome",
300
- detail: `a janela de 24h esta fechada e o agente nao declara template para ${execution.state}${execution.reason ? ` (${execution.reason})` : ""}`,
345
+ detail: `a janela de 24h esta fechada e o agente nao declara template para ${execution.state}${execution.reason ? ` (${execution.reason})` : ""}, nem um template de reserva`,
301
346
  };
302
347
  }
303
348
  const declared = channel.declaredTemplate(chosen.template);
304
349
  if (!declared) {
305
350
  return { told: false, reason: "no_template_for_outcome", detail: `o kit pediu o template ${chosen.template}, que channels/whatsapp/templates.json nao declara` };
306
351
  }
307
- if (!s.engine.markTold(execution.id, execution.state)) return { told: false, reason: "already_told" };
308
- const sent = await channel.send({ kind: "template", template: declared.name, language: declared.language, variables: chosen.variables });
309
- s.engine.note("message.debtor", execution.id, { state: execution.state, reason: execution.reason ?? null, template: declared.name, variables: chosen.variables });
310
- return deliveryOf(sent, "template", declared.name);
352
+ if (!options.cursorHeld && !s.engine.markTold(execution.id, execution.state)) return toldBefore(s, execution);
353
+ const sent = await channel.send({ kind: "template", template: declared.name, language: declared.language, variables: chosen.variables, about: aboutOf(execution) });
354
+ s.engine.note("message.debtor", execution.id, { state: execution.state, reason: execution.reason ?? null, template: declared.name, variables: chosen.variables, ...(chosen.fallback ? { fallback: true } : {}) });
355
+ return confirmed(channel, sent, deliveryOf(sent, "template", declared.name, chosen.fallback));
356
+ }
357
+
358
+ function fallbackOf(channel: WhatsAppChannel): { template: string; variables: string[]; fallback: true } | undefined {
359
+ const name = channel.fallbackTemplate();
360
+ return name ? { template: name, variables: [], fallback: true } : undefined;
311
361
  }
312
362
 
313
- function deliveryOf(sent: SentMessage, carrier: "text" | "template", template?: string): Delivery {
363
+ function deliveryOf(sent: SentMessage, carrier: "text" | "template", template?: string, fallback?: boolean): Delivery {
314
364
  if (sent.refused) return { told: false, reason: "refused", detail: `${sent.refused.rule}: ${sent.refused.detail}` };
315
365
  if (sent.state === "failed") return { told: false, reason: "refused", detail: "o provedor nao aceitou a mensagem" };
316
- return { told: true, carrier, ...(template ? { template } : {}) };
366
+ return { told: true, carrier, ...(template ? { template } : {}), ...(fallback ? { fallback: true as const } : {}) };
317
367
  }
@@ -18,7 +18,7 @@ import { join } from "node:path";
18
18
  import { loadManifest, resolveFixedClock, type ChannelName } from "@codespar/agent-core";
19
19
  import type { Agent } from "../agent.js";
20
20
  import { setup } from "../setup.js";
21
- import { announceOutcome, waitForPayer } from "../terminal.js";
21
+ import { announceOutcome, followUp, waitForPayer } from "../terminal.js";
22
22
  import { pollWhatsApp } from "./poll-whatsapp.js";
23
23
  import type { WhatsAppBackendName } from "../channels/whatsapp/open.js";
24
24
 
@@ -132,6 +132,7 @@ export async function poll(agent: Agent, argv: string[]): Promise<number> {
132
132
  const r = await waitForPayer(open.id, { setup: s, approver: { id: s.kit.labels.defaultUser, channel: "terminal" }, say, tell, waitSeconds: args.wait, simulatePayer: args.simulatePayer });
133
133
  announceOutcome(r.execution, s, tell);
134
134
  if (r.execution.state !== "executing") await s.engine.collectReceipts(r.execution.id);
135
+ await followUp(r.execution, s, say);
135
136
  results.push({ id: r.execution.id, state: r.execution.state, reason: r.execution.reason ?? null, rounds: r.rounds, seconds: r.seconds, timed_out: r.timed_out });
136
137
  say(`${r.execution.id}: ${r.execution.state}${r.execution.reason ? ` (${r.execution.reason})` : ""} after ${r.rounds} look(s), ${r.seconds}s`);
137
138
  }
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * `codespar-agent resume [--json]`: after a crash or a restart. Executions
3
- * left in `executing` are reconciled against the rail (idempotent on
4
- * attempt_id), never repeated; stale open ones expire.
3
+ * left in `executing` are reconciled against the rail, never repeated: a
4
+ * lookup presents the same explicit attempt_id, which the API answers from
5
+ * its record of that attempt without paying again (ent#1671). Stale open
6
+ * ones expire.
5
7
  */
6
8
  import { stderr, stdout } from "node:process";
7
9
  import type { Execution } from "@codespar/agent-core";
8
10
  import type { Agent } from "../agent.js";
9
11
  import { setup } from "../setup.js";
12
+ import { followUp } from "../terminal.js";
10
13
 
11
14
  export async function resume(agent: Agent, argv: string[]): Promise<number> {
12
15
  const json = argv.includes("--json");
@@ -21,6 +24,9 @@ export async function resume(agent: Agent, argv: string[]): Promise<number> {
21
24
  closed.push(e);
22
25
  say(e.state === "executing" ? s.kit.labels.stillExecuting(e) : `${e.id}: executing -> ${e.state}`);
23
26
  }
27
+ for (const e of closed) if (e.state !== "executing") await followUp(e, s, say);
28
+ // What a crash left half-done after an outcome (a service invoice opened and never sent, or sent and never answered).
29
+ await s.kit.resumeFollowUps?.(s, say);
24
30
  const expired = s.engine.expireStale().map((e) => ({ id: e.id, state: e.state }));
25
31
  for (const e of expired) say(`${e.id}: -> expired`);
26
32
  const open = s.engine.list({ state: ["awaiting_approval", "approved"] }).map((e) => ({ id: e.id, state: e.state }));
@@ -109,6 +109,9 @@ export async function startWhatsApp(options: StartWhatsAppOptions): Promise<numb
109
109
  messages_in: log.filter((l) => l.direction === "in").length,
110
110
  messages_out: log.filter((l) => l.direction === "out" && !l.refused).length,
111
111
  refused,
112
+ // What the provider reported undelivered after it left, and which outcome each one was telling.
113
+ delivery_failed: log.filter((l) => l.direction === "status" && l.state === "failed").map((l) => ({ message_id: l.message_id, errors: l.errors ?? [], ...(l.about ? { about: l.about } : {}) })),
114
+ taps: log.filter((l) => l.kind === "reply").map((l) => ({ id: l.reply?.id ?? null, turn: !l.refused, ...(l.refused ? { refused: l.refused.rule } : {}) })),
112
115
  log: relative(process.cwd(), `${s.bundle.dir}/channel.jsonl`),
113
116
  ...(cost ? { simulated_cost: cost } : {}),
114
117
  };