toolroll 0.9.53 → 0.9.55

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 (254) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +5 -4
  3. package/dist/agent-onboard.js +4 -1
  4. package/dist/approval-rules-ui.d.ts +3 -2
  5. package/dist/approval-rules-ui.js +15 -20
  6. package/dist/assignment-ui.d.ts +3 -2
  7. package/dist/assignment-ui.js +4 -11
  8. package/dist/backup-ui.d.ts +2 -1
  9. package/dist/backup-ui.js +16 -27
  10. package/dist/brand-mark.d.ts +3 -2
  11. package/dist/brand-mark.js +4 -4
  12. package/dist/browser/workspace.css +1 -1
  13. package/dist/browser/workspace.js +25 -25
  14. package/dist/browser-shell.d.ts +2 -1
  15. package/dist/browser-shell.js +9 -9
  16. package/dist/browser-workspace.d.ts +2 -3
  17. package/dist/browser-workspace.js +0 -3
  18. package/dist/chat-ask.js +8 -8
  19. package/dist/chat-core.d.ts +171 -0
  20. package/dist/chat-core.js +288 -0
  21. package/dist/chat-decide.d.ts +10 -8
  22. package/dist/chat-decide.js +26 -21
  23. package/dist/chat-delivery-state.d.ts +51 -75
  24. package/dist/chat-delivery-state.js +140 -170
  25. package/dist/chat-delivery.d.ts +21 -2
  26. package/dist/chat-delivery.js +313 -194
  27. package/dist/chat-flow.d.ts +29 -1
  28. package/dist/chat-flow.js +24 -50
  29. package/dist/chat-messages.d.ts +319 -0
  30. package/dist/chat-messages.js +841 -0
  31. package/dist/chat-migration.d.ts +30 -0
  32. package/dist/chat-migration.js +802 -0
  33. package/dist/chat-polish.d.ts +5 -4
  34. package/dist/chat-polish.js +9 -10
  35. package/dist/chat-quiet.d.ts +3 -0
  36. package/dist/chat-quiet.js +5 -0
  37. package/dist/check-levels-ui.d.ts +2 -1
  38. package/dist/check-levels-ui.js +26 -53
  39. package/dist/cli.d.ts +3 -1
  40. package/dist/cli.js +9 -3
  41. package/dist/coding-shipping-ui.d.ts +2 -1
  42. package/dist/coding-shipping-ui.js +6 -6
  43. package/dist/coding-ui.d.ts +3 -1
  44. package/dist/coding-ui.js +30 -29
  45. package/dist/contracts/chat-content.d.ts +71 -2
  46. package/dist/contracts/chat-content.js +22 -3
  47. package/dist/contracts/chat-tables.d.ts +11 -4
  48. package/dist/contracts/chat-tables.js +165 -87
  49. package/dist/contracts/cli.d.ts +1 -3
  50. package/dist/contracts/console-api.d.ts +1 -1
  51. package/dist/contracts/console-api.js +6 -5
  52. package/dist/contracts/discord-callback.js +2 -1
  53. package/dist/contracts/health.d.ts +1 -3
  54. package/dist/contracts/slack-callback.js +3 -2
  55. package/dist/contracts/store-json.d.ts +1 -5
  56. package/dist/contracts/store-json.js +1 -4
  57. package/dist/contracts/teams-callback.js +2 -1
  58. package/dist/control-ui.d.ts +5 -4
  59. package/dist/control-ui.js +9 -34
  60. package/dist/credentials-ui.d.ts +5 -4
  61. package/dist/credentials-ui.js +23 -24
  62. package/dist/demo-chat.d.ts +3 -3
  63. package/dist/demo-chat.js +29 -76
  64. package/dist/discord-chat.js +19 -38
  65. package/dist/discord-settings.d.ts +3 -2
  66. package/dist/discord-settings.js +21 -55
  67. package/dist/discord.js +4 -14
  68. package/dist/envelope.js +10 -0
  69. package/dist/evidence-pack.d.ts +2 -1
  70. package/dist/evidence-pack.js +45 -49
  71. package/dist/export-ui.d.ts +7 -2
  72. package/dist/export-ui.js +4 -10
  73. package/dist/export.js +4 -4
  74. package/dist/flow-gallery-ui.d.ts +4 -3
  75. package/dist/flow-gallery-ui.js +28 -43
  76. package/dist/flow-starters-ui.d.ts +2 -1
  77. package/dist/flow-starters-ui.js +12 -15
  78. package/dist/flow-triggers.d.ts +1 -0
  79. package/dist/flow-triggers.js +16 -14
  80. package/dist/flows-ui.d.ts +4 -3
  81. package/dist/flows-ui.js +19 -30
  82. package/dist/flows.js +2 -1
  83. package/dist/guides.js +108 -3
  84. package/dist/html.d.ts +76 -0
  85. package/dist/html.js +173 -0
  86. package/dist/integrations-ui.d.ts +2 -1
  87. package/dist/integrations-ui.js +19 -23
  88. package/dist/kits-ui.d.ts +5 -4
  89. package/dist/kits-ui.js +18 -20
  90. package/dist/knowledge-ui.d.ts +6 -5
  91. package/dist/knowledge-ui.js +26 -25
  92. package/dist/lead-context.d.ts +11 -1
  93. package/dist/lead-context.js +6 -8
  94. package/dist/ledger-view.d.ts +2 -1
  95. package/dist/ledger-view.js +27 -29
  96. package/dist/limits-ui.d.ts +2 -1
  97. package/dist/limits-ui.js +7 -7
  98. package/dist/live-bus.d.ts +50 -8
  99. package/dist/live-bus.js +133 -50
  100. package/dist/mate-tools.js +3 -2
  101. package/dist/mcp-oauth.js +35 -36
  102. package/dist/models-ui.d.ts +2 -1
  103. package/dist/models-ui.js +25 -35
  104. package/dist/modes.d.ts +13 -1
  105. package/dist/modes.js +17 -1
  106. package/dist/monitoring-ui.d.ts +4 -3
  107. package/dist/monitoring-ui.js +13 -26
  108. package/dist/notification-rows.d.ts +30 -0
  109. package/dist/notification-rows.js +102 -0
  110. package/dist/openrouter-models.d.ts +2 -1
  111. package/dist/openrouter-models.js +5 -9
  112. package/dist/operate.js +16 -9
  113. package/dist/people-audit-ui.d.ts +2 -1
  114. package/dist/people-audit-ui.js +13 -13
  115. package/dist/plane-review.js +5 -11
  116. package/dist/policy-ui.d.ts +3 -2
  117. package/dist/policy-ui.js +17 -23
  118. package/dist/project-delete-ui.d.ts +4 -3
  119. package/dist/project-delete-ui.js +15 -27
  120. package/dist/project-delete.js +21 -26
  121. package/dist/pull-request-ui.d.ts +2 -1
  122. package/dist/pull-request-ui.js +17 -32
  123. package/dist/recipe-ui.d.ts +8 -7
  124. package/dist/recipe-ui.js +39 -32
  125. package/dist/release-adapters.d.ts +60 -0
  126. package/dist/release-adapters.js +435 -0
  127. package/dist/release-cli.d.ts +24 -0
  128. package/dist/release-cli.js +181 -0
  129. package/dist/release-gate.d.ts +80 -0
  130. package/dist/release-gate.js +150 -0
  131. package/dist/release-links.d.ts +29 -0
  132. package/dist/release-links.js +102 -0
  133. package/dist/release.d.ts +331 -0
  134. package/dist/release.js +550 -0
  135. package/dist/reply-shape.d.ts +2 -1
  136. package/dist/reply-shape.js +5 -5
  137. package/dist/repository-context-ui.d.ts +2 -1
  138. package/dist/repository-context-ui.js +2 -7
  139. package/dist/request-budget-ui.d.ts +2 -1
  140. package/dist/request-budget-ui.js +16 -16
  141. package/dist/result-review.d.ts +2 -3
  142. package/dist/result-review.js +3 -9
  143. package/dist/result-shots.js +2 -2
  144. package/dist/retention-ui.d.ts +7 -1
  145. package/dist/retention-ui.js +7 -11
  146. package/dist/retention.js +1 -1
  147. package/dist/serve.d.ts +11 -2
  148. package/dist/serve.js +69 -58
  149. package/dist/server/chat.d.ts +3 -6
  150. package/dist/server/chat.js +969 -1006
  151. package/dist/server/chrome.d.ts +291 -0
  152. package/dist/server/chrome.js +747 -0
  153. package/dist/server/console.css +2819 -0
  154. package/dist/server/flows.d.ts +2 -6
  155. package/dist/server/flows.js +840 -766
  156. package/dist/server/guards.d.ts +1 -1
  157. package/dist/server/guards.js +5 -2
  158. package/dist/server/handler-context.d.ts +1 -1
  159. package/dist/server/handler-context.js +1 -1
  160. package/dist/server/handler-registry.d.ts +8 -6
  161. package/dist/server/handler-registry.js +14 -2
  162. package/dist/server/http.d.ts +274 -0
  163. package/dist/server/http.js +239 -0
  164. package/dist/server/live.d.ts +34 -0
  165. package/dist/server/live.js +309 -0
  166. package/dist/server/pages.d.ts +2 -6
  167. package/dist/server/pages.js +808 -789
  168. package/dist/server/people-tokens.d.ts +1 -6
  169. package/dist/server/people-tokens.js +580 -572
  170. package/dist/server/remote.d.ts +4 -7
  171. package/dist/server/remote.js +37 -70
  172. package/dist/server/render-chat.d.ts +402 -0
  173. package/dist/server/render-chat.js +927 -0
  174. package/dist/server/render-pages.d.ts +535 -0
  175. package/dist/server/render-pages.js +1277 -0
  176. package/dist/server/render-people.d.ts +32 -0
  177. package/dist/server/render-people.js +121 -0
  178. package/dist/server/render-results.d.ts +744 -0
  179. package/dist/server/render-results.js +2237 -0
  180. package/dist/server/render-settings.d.ts +147 -0
  181. package/dist/server/render-settings.js +577 -0
  182. package/dist/server/render-tasks.d.ts +632 -0
  183. package/dist/server/render-tasks.js +2345 -0
  184. package/dist/server/request-context.d.ts +46 -0
  185. package/dist/server/request-context.js +17 -0
  186. package/dist/server/route-table.d.ts +25 -3
  187. package/dist/server/route-table.js +163 -75
  188. package/dist/server/runtime.d.ts +15 -7
  189. package/dist/server/runtime.js +7 -1
  190. package/dist/server/session.d.ts +138 -0
  191. package/dist/server/session.js +94 -0
  192. package/dist/server/settings.d.ts +1 -5
  193. package/dist/server/settings.js +2182 -2117
  194. package/dist/server/task-views.d.ts +11 -4
  195. package/dist/server/task-views.js +26 -22
  196. package/dist/server/tasks.d.ts +9 -9
  197. package/dist/server/tasks.js +1486 -1418
  198. package/dist/server-telemetry.d.ts +4 -3
  199. package/dist/server-telemetry.js +4 -8
  200. package/dist/skills-ui.d.ts +4 -3
  201. package/dist/skills-ui.js +14 -17
  202. package/dist/skills.d.ts +1 -1
  203. package/dist/skills.js +2 -1
  204. package/dist/slack-api.d.ts +5 -1
  205. package/dist/slack-api.js +2 -2
  206. package/dist/slack-chat.d.ts +9 -4
  207. package/dist/slack-chat.js +29 -52
  208. package/dist/slack-settings.d.ts +3 -2
  209. package/dist/slack-settings.js +23 -57
  210. package/dist/slack.js +6 -16
  211. package/dist/spend-ui.d.ts +2 -1
  212. package/dist/spend-ui.js +9 -32
  213. package/dist/sso-ui.d.ts +3 -2
  214. package/dist/sso-ui.js +26 -27
  215. package/dist/storage-ui.d.ts +2 -1
  216. package/dist/storage-ui.js +14 -33
  217. package/dist/store.d.ts +97 -364
  218. package/dist/store.js +207 -1531
  219. package/dist/surface.js +14 -3
  220. package/dist/task-composer.d.ts +2 -1
  221. package/dist/task-composer.js +7 -7
  222. package/dist/task-status.d.ts +4 -3
  223. package/dist/task-status.js +15 -16
  224. package/dist/team-http.d.ts +5 -9
  225. package/dist/team-http.js +5 -63
  226. package/dist/team-ui.d.ts +2 -1
  227. package/dist/team-ui.js +2 -5
  228. package/dist/teammate-question.js +12 -12
  229. package/dist/teammates-ui.d.ts +5 -4
  230. package/dist/teammates-ui.js +61 -90
  231. package/dist/teams-chat.js +12 -9
  232. package/dist/teams-settings.d.ts +3 -2
  233. package/dist/teams-settings.js +22 -33
  234. package/dist/teams.js +4 -5
  235. package/dist/telegram-flow.js +5 -5
  236. package/dist/telegram-settings.d.ts +3 -2
  237. package/dist/telegram-settings.js +14 -25
  238. package/dist/telegram.js +4 -4
  239. package/dist/toolroll-update-ui.d.ts +3 -2
  240. package/dist/toolroll-update-ui.js +35 -39
  241. package/dist/toolroll-update.d.ts +10 -0
  242. package/dist/toolroll-update.js +50 -3
  243. package/dist/tools-ui.d.ts +5 -2
  244. package/dist/tools-ui.js +31 -31
  245. package/dist/when-html.d.ts +11 -3
  246. package/dist/when-html.js +5 -6
  247. package/dist/workspace-revision.js +3 -1
  248. package/dist/workspace-ui.d.ts +4 -2
  249. package/dist/workspace-ui.js +10 -10
  250. package/package.json +1 -1
  251. package/dist/server/shared.d.ts +0 -3151
  252. package/dist/server/shared.js +0 -12001
  253. package/dist/slack-state.d.ts +0 -13
  254. package/dist/slack-state.js +0 -10
@@ -1,10 +1,38 @@
1
+ /**
2
+ * Flow decisions in Slack, Discord and Teams (v88): the Approve / Edit /
3
+ * Send back that Telegram has (telegram-flow.ts), on the notice itself.
4
+ *
5
+ * - The notice carries the draft (split over as many parts as it needs, so
6
+ * what is approved is what was read) and the buttons on its last part.
7
+ * - Approve decides through the same door as the console (decideFlowCard),
8
+ * as the paired person, for exactly the visit the notice was about, then
9
+ * repaints the notice with what happened and no buttons.
10
+ * - Edit and Send back open a prompt: the person's next message in their
11
+ * chat with Toolroll is the new draft (which comes back with fresh
12
+ * buttons) or the note. "cancel" leaves it, and a prompt lapses after
13
+ * 30 minutes. Chat apps can't all name the message a reply answers, so
14
+ * the prompt says plainly what the next message does.
15
+ *
16
+ * Every button is one opaque token for one visit of one card, on the part it
17
+ * rides: a tap on any other message, from anyone but the paired person, or
18
+ * for a card that moved on, changes nothing.
19
+ *
20
+ * A "Send to me" visit (flow-send.ts) is one notice with its links; a "Person
21
+ * chooses" visit is the same with the zone's own options as buttons. Instead
22
+ * of a tap, a reply in the notice's thread is the note for where replies go;
23
+ * the first other message in their chat is asked about once ("Use this as
24
+ * your note?"), and everything else goes to the lead as usual. Choices are
25
+ * made through chooseFlowCard, by that person alone, for exactly that visit,
26
+ * and spend that visit's buttons in every chat app.
27
+ */
28
+ import type { ChatProvider } from "./contracts/chat-tables.js";
1
29
  import { type ChatBinding, type ChatContent, type ChatEvent, type ChatState } from "./chat-delivery-state.js";
2
30
  import type { Store } from "./store.js";
3
31
  export type ChatFlowAction = "approve" | "edit" | "send-back";
4
32
  export declare const CHAT_FLOW_LABELS: Record<ChatFlowAction, string>;
5
33
  /** Words kept as written, line breaks and all; only control characters go. */
6
34
  export declare function chatFlowText(text: string): string;
7
- type ChatApp = "slack" | "discord" | "teams";
35
+ type ChatApp = ChatProvider;
8
36
  /** A "Send to me" notice as chat parts: what was done, then its links; null when it isn't one. */
9
37
  export declare function flowSendParts(store: Store, notification: {
10
38
  dedupeKey: string;
package/dist/chat-flow.js CHANGED
@@ -1,30 +1,3 @@
1
- /**
2
- * Flow decisions in Slack, Discord and Teams (v88): the Approve / Edit /
3
- * Send back that Telegram has (telegram-flow.ts), on the notice itself.
4
- *
5
- * - The notice carries the draft (split over as many parts as it needs, so
6
- * what is approved is what was read) and the buttons on its last part.
7
- * - Approve decides through the same door as the console (decideFlowCard),
8
- * as the paired person, for exactly the visit the notice was about, then
9
- * repaints the notice with what happened and no buttons.
10
- * - Edit and Send back open a prompt: the person's next message in their
11
- * chat with Toolroll is the new draft (which comes back with fresh
12
- * buttons) or the note. "cancel" leaves it, and a prompt lapses after
13
- * 30 minutes. Chat apps can't all name the message a reply answers, so
14
- * the prompt says plainly what the next message does.
15
- *
16
- * Every button is one opaque token for one visit of one card, on the part it
17
- * rides: a tap on any other message, from anyone but the paired person, or
18
- * for a card that moved on, changes nothing.
19
- *
20
- * A "Send to me" visit (flow-send.ts) is one notice with its links; a "Person
21
- * chooses" visit is the same with the zone's own options as buttons. Instead
22
- * of a tap, a reply in the notice's thread is the note for where replies go;
23
- * the first other message in their chat is asked about once ("Use this as
24
- * your note?"), and everything else goes to the lead as usual. Choices are
25
- * made through chooseFlowCard, by that person alone, for exactly that visit,
26
- * and spend that visit's buttons in every chat app.
27
- */
28
1
  import { LIMITS } from "./decision.js";
29
2
  import { keptDraft } from "./flow-draft.js";
30
3
  import { decideFlowCard, flowCardHref, flowDefinitionOf } from "./flow-engine.js";
@@ -77,7 +50,7 @@ function sendLinks(paths) {
77
50
  }
78
51
  /** The most an item list's part carries here, as the app renders it — a Slack section (2,800), Discord's plain message
79
52
  * content (2,000), a Teams message as long as Telegram's — less room for the line a tap adds under it. */
80
- const ITEMS_LIMIT = { slack: PART_CHARS - 300, discord: 2000 - 300, teams: 4096 - 300 };
53
+ const ITEMS_LIMIT = { slack: PART_CHARS - 300, discord: 2000 - 300, teams: 4096 - 300, telegram: 4096 - 300 };
81
54
  /** After research (flow-items.ts): the message with each item numbered under the summary, its words cleaned as every
82
55
  * other flow message's are, in as few parts as the app's limit allows (one, unless head, links and tail alone overflow). */
83
56
  function itemsText(content, subject, app) {
@@ -129,27 +102,27 @@ export function flowDecisionParts(store, notification, app) {
129
102
  /** The live buttons on one part, in the order they were minted: a decision's, a choice's options in their order, or
130
103
  * "Use this as your note?" Yes / No. `key` is unique on the part (Slack names each button by it). */
131
104
  export function chatFlowButtons(state, part, now) {
132
- const decide = state.prepare("SELECT token,action FROM chat_flow_action WHERE part=? AND consumed IS NULL AND expires>? ORDER BY rowid").all(part, now.toISOString())
105
+ const decide = state.prepare("SELECT token,action FROM chat_flow_action WHERE provider=:provider AND part=? AND consumed IS NULL AND expires>? ORDER BY rowid").all(part, now.toISOString())
133
106
  .map(one => ({ token: String(one.token), action: one.action, label: CHAT_FLOW_LABELS[one.action], key: one.action.replace("-", "_") }));
134
- const choose = state.prepare("SELECT token,choice,label FROM chat_flow_choice WHERE part=? AND consumed IS NULL AND expires>? ORDER BY choice").all(part, now.toISOString())
107
+ const choose = state.prepare("SELECT token,choice,label FROM chat_flow_choice WHERE provider=:provider AND part=? AND consumed IS NULL AND expires>? ORDER BY choice").all(part, now.toISOString())
135
108
  .map(one => ({ token: String(one.token), action: "choose", label: String(one.label), key: `choose_${Number(one.choice)}` }));
136
- const note = state.prepare("SELECT token,answer FROM chat_flow_note WHERE part=? AND consumed IS NULL AND expires>? ORDER BY answer DESC").all(part, now.toISOString())
109
+ const note = state.prepare("SELECT token,answer FROM chat_flow_note WHERE provider=:provider AND part=? AND consumed IS NULL AND expires>? ORDER BY answer DESC").all(part, now.toISOString())
137
110
  .map(one => ({ token: String(one.token), action: "note", label: one.answer === "yes" ? "Yes" : "No", key: `note_${one.answer}` }));
138
111
  return [...decide, ...choose, ...note];
139
112
  }
140
113
  const retire = (state, card, entry, now) => {
141
- state.prepare("UPDATE chat_flow_action SET consumed=? WHERE card=? AND entry=? AND consumed IS NULL").run(now.toISOString(), card, entry);
142
- state.prepare("UPDATE chat_flow_choice SET consumed=? WHERE card=? AND entry=? AND consumed IS NULL").run(now.toISOString(), card, entry);
143
- state.prepare("UPDATE chat_flow_prompt SET consumed=? WHERE card=? AND entry=? AND consumed IS NULL").run(now.toISOString(), card, entry);
114
+ state.prepare("UPDATE chat_flow_action SET consumed=? WHERE provider=:provider AND card=? AND entry=? AND consumed IS NULL").run(now.toISOString(), card, entry);
115
+ state.prepare("UPDATE chat_flow_choice SET consumed=? WHERE provider=:provider AND card=? AND entry=? AND consumed IS NULL").run(now.toISOString(), card, entry);
116
+ state.prepare("UPDATE chat_flow_prompt SET consumed=? WHERE provider=:provider AND card=? AND entry=? AND consumed IS NULL").run(now.toISOString(), card, entry);
144
117
  };
145
118
  /** Show the tapped notice again with what happened, and without its buttons. */
146
119
  function repaint(state, part, event, line) {
147
- const row = state.prepare("SELECT payload FROM chat_part WHERE id=?").get(part);
120
+ const row = state.prepare("SELECT payload FROM chat_part WHERE provider=:provider AND id=?").get(part);
148
121
  const before = row === undefined ? { text: "" } : partContent(String(row["payload"]));
149
122
  // An item list (flow-items.ts) stays whole and keeps its bold titles and labelled links; only plain text is capped.
150
123
  const content = { text: before.voice === true ? `${before.text}\n\n${line}` : `${before.text}\n\n${line}`.slice(0, PART_CHARS + 400), edit: event.ts, ...(before.voice === true ? { voice: true } : {}),
151
124
  ...(before.link === undefined ? {} : { link: before.link }), ...(before.channel === undefined ? {} : { channel: before.channel }) };
152
- state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?").run(savedChatPart(content), part);
125
+ state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE provider=:provider AND id=?").run(savedChatPart(content), part);
153
126
  }
154
127
  /**
155
128
  * A tapped flow button, inside the action's transaction. false when the
@@ -158,7 +131,7 @@ function repaint(state, part, event, line) {
158
131
  */
159
132
  export function applyChatFlowTap(options, event, binding, token, repos, now) {
160
133
  const { store, state } = options;
161
- const action = state.prepare("SELECT a.*,p.message,e.binding AS owner,e.channel FROM chat_flow_action a JOIN chat_part p ON p.id=a.part JOIN chat_event e ON e.id=p.event WHERE a.token=?").get(token);
134
+ const action = state.prepare("SELECT a.token,a.part,a.card,a.entry,a.action,a.expires,a.consumed,p.message,e.binding AS owner,e.channel FROM chat_flow_action a JOIN chat_part p ON p.provider=a.provider AND p.id=a.part JOIN chat_event e ON e.provider=p.provider AND e.id=p.event WHERE a.provider=:provider AND a.token=?").get(token);
162
135
  if (action === undefined)
163
136
  return applyChatChoiceTap(options, event, binding, token, repos, now) || applyChatNoteTap(options, event, binding, token, repos, now);
164
137
  const say = (text) => state.plan(event.id, [{ text }], now);
@@ -202,9 +175,9 @@ export function applyChatFlowTap(options, event, binding, token, repos, now) {
202
175
  return true;
203
176
  }
204
177
  // One open prompt per person: the newest question is the one the next message answers.
205
- state.prepare("UPDATE chat_flow_prompt SET consumed=? WHERE binding=? AND consumed IS NULL").run(now.toISOString(), binding.id);
206
- state.prepare("UPDATE chat_question_prompt SET consumed=? WHERE binding=? AND consumed IS NULL").run(now.toISOString(), binding.id);
207
- state.prepare("INSERT INTO chat_flow_prompt(binding,card,entry,mode,created,expires) VALUES(?,?,?,?,?,?)")
178
+ state.prepare("UPDATE chat_flow_prompt SET consumed=? WHERE provider=:provider AND binding=? AND consumed IS NULL").run(now.toISOString(), binding.id);
179
+ state.prepare("UPDATE chat_question_prompt SET consumed=? WHERE provider=:provider AND binding=? AND consumed IS NULL").run(now.toISOString(), binding.id);
180
+ state.prepare("INSERT INTO chat_flow_prompt(provider,id,binding,card,entry,mode,created,expires) VALUES(:provider,(SELECT COALESCE(MAX(id),0)+1 FROM chat_flow_prompt WHERE provider=:provider),?,?,?,?,?,?)")
208
181
  .run(binding.id, card, entry, kind, now.toISOString(), new Date(now.getTime() + PROMPT_MS).toISOString());
209
182
  const back = waiting.stage.onFail === null ? "the zone before" : flowDefinitionOf(waiting.flow)?.stages.find(one => one.id === waiting.stage.onFail)?.title ?? waiting.stage.onFail;
210
183
  say(kind === "edit"
@@ -215,7 +188,7 @@ export function applyChatFlowTap(options, event, binding, token, repos, now) {
215
188
  /** A tapped option of a "Person chooses" notice; false when the token isn't one. */
216
189
  function applyChatChoiceTap(options, event, binding, token, repos, now) {
217
190
  const { store, state } = options;
218
- const tapped = state.prepare("SELECT c.*,p.message,e.binding AS owner,e.channel FROM chat_flow_choice c JOIN chat_part p ON p.id=c.part JOIN chat_event e ON e.id=p.event WHERE c.token=?").get(token);
191
+ const tapped = state.prepare("SELECT c.token,c.part,c.card,c.entry,c.choice,c.label,c.expires,c.consumed,p.message,e.binding AS owner,e.channel FROM chat_flow_choice c JOIN chat_part p ON p.provider=c.provider AND p.id=c.part JOIN chat_event e ON e.provider=p.provider AND e.id=p.event WHERE c.provider=:provider AND c.token=?").get(token);
219
192
  if (tapped === undefined)
220
193
  return false;
221
194
  const say = (text) => state.plan(event.id, [{ text }], now);
@@ -270,8 +243,9 @@ function replyAsNote(options, binding, said, cutShort, visit, repos, now) {
270
243
  }
271
244
  /** The choice a part's message carries, by the message any of its parts was posted as (a long notice is several). */
272
245
  function choiceOnMessage(state, binding, channel, message) {
273
- const row = state.prepare(`SELECT c.card,c.entry,MAX(c.consumed IS NULL) AS live FROM chat_part p JOIN chat_event e ON e.id=p.event
274
- JOIN chat_part q ON q.event=p.event JOIN chat_flow_choice c ON c.part=q.id WHERE e.binding=? AND e.channel=? AND p.message=? GROUP BY c.card,c.entry ORDER BY MAX(c.rowid) DESC LIMIT 1`)
246
+ const row = state.prepare(`SELECT c.card,c.entry,MAX(c.consumed IS NULL) AS live FROM chat_part p JOIN chat_event e ON e.provider=p.provider AND e.id=p.event
247
+ JOIN chat_part q ON q.provider=p.provider AND q.event=p.event JOIN chat_flow_choice c ON c.provider=q.provider AND c.part=q.id
248
+ WHERE p.provider=:provider AND e.binding=? AND e.channel=? AND p.message=? GROUP BY c.card,c.entry ORDER BY MAX(c.rowid) DESC LIMIT 1`)
275
249
  .get(binding.id, channel, message);
276
250
  return row === undefined ? null : { card: Number(row["card"]), entry: Number(row["entry"]), live: Number(row["live"]) === 1 };
277
251
  }
@@ -315,8 +289,8 @@ export function answerChoiceMessage(options, event, binding, input, now, repos)
315
289
  if (said === "" || phoneCommand(said) !== null)
316
290
  return false;
317
291
  // The newest choice of theirs still open here that takes a reply and hasn't asked yet.
318
- const open = state.prepare(`SELECT DISTINCT c.card,c.entry FROM chat_flow_choice c JOIN chat_part p ON p.id=c.part JOIN chat_event e ON e.id=p.event
319
- WHERE e.binding=? AND c.consumed IS NULL AND c.expires>? AND NOT EXISTS (SELECT 1 FROM chat_flow_note n WHERE n.card=c.card AND n.entry=c.entry) ORDER BY c.rowid DESC`)
292
+ const open = state.prepare(`SELECT DISTINCT c.card,c.entry FROM chat_flow_choice c JOIN chat_part p ON p.provider=c.provider AND p.id=c.part JOIN chat_event e ON e.provider=p.provider AND e.id=p.event
293
+ WHERE c.provider=:provider AND e.binding=? AND c.consumed IS NULL AND c.expires>? AND NOT EXISTS (SELECT 1 FROM chat_flow_note n WHERE n.provider=c.provider AND n.card=c.card AND n.entry=c.entry) ORDER BY c.rowid DESC`)
320
294
  .all(binding.id, now.toISOString());
321
295
  for (const one of open) {
322
296
  const visit = flowChoiceAt(store, Number(one.card), Number(one.entry));
@@ -327,7 +301,7 @@ export function answerChoiceMessage(options, event, binding, input, now, repos)
327
301
  state.plan(event.id, [{ text: `Use this as your note on “${visit.card.title}”?\nYes sends it to ${to}. No passes it to the lead.`,
328
302
  note: { card: visit.card.id, entry: visit.card.entry, held: event.id } }], now);
329
303
  // What it said is kept beside its question until answered: the event's own copy is cleared once it is handled.
330
- state.prepare("UPDATE chat_flow_note SET words=? WHERE held=?").run(JSON.stringify({ text: input.text, ...(input.originalLength === undefined ? {} : { originalLength: input.originalLength }) }), event.id);
304
+ state.prepare("UPDATE chat_flow_note SET words=? WHERE provider=:provider AND held=?").run(JSON.stringify({ text: input.text, ...(input.originalLength === undefined ? {} : { originalLength: input.originalLength }) }), event.id);
331
305
  return true;
332
306
  }
333
307
  return false;
@@ -336,7 +310,7 @@ export function answerChoiceMessage(options, event, binding, input, now, repos)
336
310
  /** Yes or No to "Use this as your note?"; false when the token isn't one. */
337
311
  function applyChatNoteTap(options, event, binding, token, repos, now) {
338
312
  const { store, state } = options;
339
- const tapped = state.prepare("SELECT n.*,p.message,e.binding AS owner,e.channel FROM chat_flow_note n JOIN chat_part p ON p.id=n.part JOIN chat_event e ON e.id=p.event WHERE n.token=?").get(token);
313
+ const tapped = state.prepare("SELECT n.token,n.part,n.card,n.entry,n.held,n.words,n.answer,n.expires,n.consumed,p.message,e.binding AS owner,e.channel FROM chat_flow_note n JOIN chat_part p ON p.provider=n.provider AND p.id=n.part JOIN chat_event e ON e.provider=p.provider AND e.id=p.event WHERE n.provider=:provider AND n.token=?").get(token);
340
314
  if (tapped === undefined)
341
315
  return false;
342
316
  const say = (text) => state.plan(event.id, [{ text }], now);
@@ -350,7 +324,7 @@ function applyChatNoteTap(options, event, binding, token, repos, now) {
350
324
  state.finish(event.id);
351
325
  return true;
352
326
  }
353
- state.prepare("UPDATE chat_flow_note SET consumed=?,words=NULL WHERE part=?").run(now.toISOString(), part);
327
+ state.prepare("UPDATE chat_flow_note SET consumed=?,words=NULL WHERE provider=:provider AND part=?").run(now.toISOString(), part);
354
328
  const held = state.event(String(tapped["held"]));
355
329
  const kept = typeof tapped["words"] === "string" ? readHeldWords(tapped["words"]) : null;
356
330
  const words = kept?.ok === true ? kept.value : null;
@@ -390,12 +364,12 @@ export function answerChatFlowPrompt(options, event, binding, input, repos, now)
390
364
  if (event.channel !== binding.channel)
391
365
  return false;
392
366
  return store.transact(() => {
393
- const prompt = state.prepare("SELECT * FROM chat_flow_prompt WHERE binding=? AND consumed IS NULL AND expires>? ORDER BY id DESC LIMIT 1").get(binding.id, now.toISOString());
367
+ const prompt = state.prepare("SELECT * FROM chat_flow_prompt WHERE provider=:provider AND binding=? AND consumed IS NULL AND expires>? ORDER BY id DESC LIMIT 1").get(binding.id, now.toISOString());
394
368
  if (prompt === undefined)
395
369
  return false;
396
370
  const id = Number(prompt["id"]), card = Number(prompt["card"]), entry = Number(prompt["entry"]), mode = String(prompt["mode"]);
397
371
  const say = (text) => state.plan(event.id, [{ text }], now);
398
- const close = () => state.prepare("UPDATE chat_flow_prompt SET consumed=? WHERE id=?").run(now.toISOString(), id);
372
+ const close = () => state.prepare("UPDATE chat_flow_prompt SET consumed=? WHERE provider=:provider AND id=?").run(now.toISOString(), id);
399
373
  const said = input.text.trim();
400
374
  if (/^cancel\.?$/i.test(said)) {
401
375
  close();
@@ -0,0 +1,319 @@
1
+ import { ChatCore } from "./chat-core.js";
2
+ import type { Notification, NotificationReceipt, Run, TelegramAction, TelegramBinding, TelegramConversation, TelegramConversationPart, TelegramConversationPartPlan, TelegramDelivery, TelegramDigest, TelegramFlowAction, TelegramFlowChoice, TelegramFlowPrompt, TelegramProposalAction } from "./store.js";
3
+ /** v83: a notification for another person, settled for this destination without sending. */
4
+ export declare const TELEGRAM_SKIPPED_ELSEWHERE = "skipped:for-another-person";
5
+ /** A met promise the lead made on another chat: reported there, settled here without sending. */
6
+ export declare const TELEGRAM_SKIPPED_OTHER_CHAT = "skipped:for-another-chat";
7
+ /** A fact this person is not messaged about: the lead's work, their own act, or a project they muted. */
8
+ export declare const TELEGRAM_SKIPPED_QUIET = "skipped:quiet";
9
+ /** A destination receipt (`d` = notification_delivery) that still owes a
10
+ * send: not delivered, and not explicitly skipped as pre-pairing history.
11
+ * Every reader of Telegram receipts that means "undelivered" uses this, so
12
+ * a skip settles the row without ever pretending to be a delivery. */
13
+ export declare const TELEGRAM_UNSETTLED = "d.delivered_at IS NULL AND (d.receipt IS NULL OR d.receipt NOT LIKE 'skipped:%')";
14
+ /**
15
+ * Why the store itself holds a Telegram delivery back: authority,
16
+ * provenance, ordering, a switched-off channel, or a wait it imposed. A
17
+ * row held for one of these is waiting on policy, not failing on the wire.
18
+ * Every other error recorded on a receipt came from an actual send, and
19
+ * THAT is delivery trouble (`pendingForAttention`). One table, so the
20
+ * fences and the tally can never disagree about the words.
21
+ */
22
+ export declare const TELEGRAM_HOLD_REASONS: Readonly<{
23
+ authority: "Telegram pairing or actor authorization changed";
24
+ destination: "Telegram destination changed";
25
+ claim: "Telegram delivery claim expired or changed";
26
+ rateLimit: "Telegram rate limit is still active";
27
+ resolved: "Notification no longer needs delivery";
28
+ provenance: "Notification has no trusted project provenance";
29
+ installation: "Installation notification requires instance access";
30
+ project: "Notification project is not currently authorized and enrolled";
31
+ task: "Notification task provenance changed";
32
+ order: "Earlier task notification is still undelivered";
33
+ disabled: "Telegram delivery is disabled";
34
+ }>;
35
+ export declare class ChatMessages extends ChatCore {
36
+ createDecisionAction(action: {
37
+ token: string;
38
+ binding: number;
39
+ decision: number;
40
+ optionId: string;
41
+ phase: "choose" | "confirm" | "cancel";
42
+ chatId: string;
43
+ messageId?: string;
44
+ ttlMs?: number;
45
+ /** Binds an irreversible confirmation to the EXACT note it showed. */
46
+ noteDigest?: string;
47
+ }, now: Date): void;
48
+ /** Which decision an outbound Telegram message carried. Recorded after the
49
+ * send returns its id; a send whose record was lost routes nothing. */
50
+ recordDecisionMessage(binding: number, chatId: string, messageId: string, decision: number, now: Date): void;
51
+ /** The decision behind an exact replied-to message, or nothing. Never "the latest". */
52
+ decisionForMessage(binding: number, chatId: string, messageId: string): number | null;
53
+ /** The one live, unexpired draft for a decision, if any. */
54
+ liveNoteDraft(binding: number, decision: number, now: Date): {
55
+ id: number;
56
+ note: string;
57
+ updateId: number;
58
+ state: string;
59
+ } | null;
60
+ /**
61
+ * Persist a validated note as the live draft. A newer update SUPERSEDES
62
+ * the old draft — drafts are immutable, and only a GREATER update_id may
63
+ * replace one, so out-of-order delivery cannot resurrect an older note
64
+ * (Codex free-text review, state machine). Returns false when an older
65
+ * or equal update tried.
66
+ */
67
+ saveNoteDraft(draft: {
68
+ binding: number;
69
+ decision: number;
70
+ updateId: number;
71
+ messageId: string;
72
+ replyTo: string;
73
+ note: string;
74
+ }, now: Date, ttlMs?: number): boolean;
75
+ /** Move a draft between states; the caller owns the transaction story. */
76
+ setNoteDraftState(id: number, state: "pending" | "armed" | "superseded" | "consumed" | "discarded"): void;
77
+ createFlowActions(at: {
78
+ binding: number;
79
+ chatId: string;
80
+ card: number;
81
+ entry: number;
82
+ }, actions: readonly {
83
+ token: string;
84
+ action: TelegramFlowAction["action"];
85
+ }[], now: Date, days?: number): void;
86
+ flowAction(token: string): TelegramFlowAction | null;
87
+ placeFlowActions(tokens: readonly string[], messageId: string): void;
88
+ /** Retire every button (and prompt) for one visit of a card, in every chat app: it was decided, or its draft replaced. */
89
+ retireFlowVisit(card: number, entry: number, now: Date): void;
90
+ recordFlowPrompt(prompt: TelegramFlowPrompt, now: Date, hours?: number): void;
91
+ /** The live prompt a message replies to, if any. */
92
+ flowPrompt(chatId: string, messageId: string, now: Date): TelegramFlowPrompt | null;
93
+ createFlowChoices(at: {
94
+ binding: number;
95
+ chatId: string;
96
+ card: number;
97
+ entry: number;
98
+ }, choices: readonly {
99
+ token: string;
100
+ choice: number;
101
+ label: string;
102
+ }[], now: Date, days?: number): void;
103
+ flowChoice(token: string): TelegramFlowChoice | null;
104
+ placeFlowChoices(tokens: readonly string[], messageId: string): void;
105
+ createQuestionActions(at: {
106
+ binding: number;
107
+ chatId: string;
108
+ question: number;
109
+ }, choices: readonly {
110
+ token: string;
111
+ choice: string | null;
112
+ }[], now: Date, days?: number): void;
113
+ questionAction(token: string): {
114
+ token: string;
115
+ binding: number;
116
+ question: number;
117
+ choice: string | null;
118
+ chatId: string;
119
+ messageId: string | null;
120
+ expiresAt: string;
121
+ consumedAt: string | null;
122
+ } | null;
123
+ placeQuestionActions(tokens: readonly string[], messageId: string): void;
124
+ /** Retire every button and prompt for a question, in every chat app: it was answered (anywhere). */
125
+ retireQuestion(question: number, now: Date): void;
126
+ recordQuestionPrompt(prompt: {
127
+ chatId: string;
128
+ messageId: string;
129
+ binding: number;
130
+ question: number;
131
+ }, now: Date, hours?: number): void;
132
+ questionPrompt(chatId: string, messageId: string, now: Date): {
133
+ chatId: string;
134
+ messageId: string;
135
+ binding: number;
136
+ question: number;
137
+ } | null;
138
+ decisionAction(token: string): TelegramAction | null;
139
+ /**
140
+ * Kill every live confirm/cancel challenge on a decision. Cancel means
141
+ * cancelled: a confirm that survived its own cancellation would be an
142
+ * irreversible choice still armed after the person said stop.
143
+ */
144
+ consumeChallenges(decision: number, now: Date): void;
145
+ /** Consume once. False means somebody already did, or it expired — either way, no. */
146
+ consumeDecisionAction(token: string, now: Date): boolean;
147
+ /** Stamp the message a choose-action's keyboard actually landed on. */
148
+ placeDecisionActions(tokens: readonly string[], messageId: string): void;
149
+ /** A Telegram message this bot sent that showed one task (v79): a reply to
150
+ * it is about that task (and that result, when it showed one). A message
151
+ * edited to show something else forgets it. */
152
+ recordTaskMessage(binding: TelegramBinding, messageId: string, taskId: string | null, run: number | null, now: Date): void;
153
+ /** v98: an update Telegram pushed, kept (queued, with its body) until the bridge applies it; false when it's already kept or applied. */
154
+ queueUpdate(botId: string, updateId: number, payload: string, now: Date): boolean;
155
+ /** v98: pushed updates waiting for the bridge, oldest first. */
156
+ inbox(botId: string, limit: number): {
157
+ updateId: number;
158
+ payload: string;
159
+ }[];
160
+ /** A pushed update the bridge is done with: applied (then its receipt stays), or unreadable (then it is gone). */
161
+ dropInbox(updateId: number): void;
162
+ /** True if this update has not been applied before. The (provider, id) key is the idempotency; a pushed update waiting
163
+ * in the inbox is applied in place. A receipt names only its update, as it always has. */
164
+ markUpdateApplied(updateId: number, result: string, now: Date): boolean;
165
+ /** Persist one ordinary message before its update is acknowledged (event m<update id>). The caller's transaction also marks the update applied. */
166
+ enqueueMessage(row: {
167
+ binding: TelegramBinding;
168
+ updateId: number;
169
+ messageId: string;
170
+ replyTo: string | null;
171
+ request: string;
172
+ text: string;
173
+ context: string | null;
174
+ taskId: string | null;
175
+ sourceRun: number | null;
176
+ }, now: Date): number;
177
+ /** Whether a message from this chat is already waiting on, or being answered from, this bot message (a tapped question). */
178
+ messageWaitingOn(binding: number, messageId: string): boolean;
179
+ getMessage(id: number): TelegramConversation | null;
180
+ listMessages(botId: string): TelegramConversation[];
181
+ /**
182
+ * Claim the oldest message that still needs a turn: queued, or running
183
+ * under a claim that lapsed (a crash mid-turn), and not deferred past now.
184
+ * One at a time per bot — the mate runs one turn per approver anyway.
185
+ */
186
+ claimMessage(botId: string, owner: string, ttlMs: number, now: Date, only?: "turns" | "replies"): TelegramConversation | null;
187
+ /** Extend a held claim; false means it lapsed and somebody else may hold the row. */
188
+ renewMessage(id: number, owner: string, ttlMs: number, now: Date): boolean;
189
+ /**
190
+ * Bind the session a turn is about to be dispatched under, BEFORE the
191
+ * dispatch: the engine receipts the request inside that session, so a
192
+ * later attempt — after a crash, even after the session was ended and
193
+ * replaced from the console — reads the receipt where it was written
194
+ * instead of resolving today's session and finding nothing there.
195
+ */
196
+ bindMessageSession(id: number, owner: string, session: number): boolean;
197
+ /** Bind the admitted turn to its row as soon as the engine's receipt names it. */
198
+ bindMessageTurn(id: number, owner: string, session: number, turn: number): boolean;
199
+ /**
200
+ * Settle a claimed row: done or failed with its outcome word, or back to
201
+ * queued for a later attempt (a busy engine). Only the claim holder may;
202
+ * a lapsed claim settles nothing, so a reclaimer's outcome stands.
203
+ */
204
+ finishMessage(id: number, owner: string, result: {
205
+ state: "done" | "failed";
206
+ outcome: string;
207
+ replyMessageId?: string | null;
208
+ } | {
209
+ state: "queued";
210
+ outcome: string;
211
+ nextAttemptAt: string;
212
+ }, now: Date): boolean;
213
+ /**
214
+ * Replies the assistant wrote that still have not reached the person
215
+ * `since` ago or longer (status and the console show them with a Retry):
216
+ * the message's row, whose it is, when the oldest unsent part was
217
+ * written, and the last send error.
218
+ */
219
+ unsentReplies(botId: string | null, approver: string | null, since: Date): {
220
+ conversation: number;
221
+ approver: string;
222
+ since: string;
223
+ error: string | null;
224
+ }[];
225
+ /** Send unsent replies on the next bridge pass instead of waiting out their backoff; returns how many. */
226
+ retryReplies(approver: string | null, now: Date): number;
227
+ /**
228
+ * Persist the whole outbound half of a turn — every reply part and every
229
+ * card, with the cards' tokens already minted — in one transaction, BEFORE
230
+ * any send. Only the claim holder may, and only once: a row that already
231
+ * has parts keeps them (a retry resumes, never re-plans). The turn's
232
+ * identity rides the row too, so the parts answer to exactly that turn.
233
+ */
234
+ planReplyParts(id: number, owner: string, turn: {
235
+ session: number;
236
+ turn: number;
237
+ }, parts: readonly TelegramConversationPartPlan[], now: Date): boolean;
238
+ replyParts(conversation: number): TelegramConversationPart[];
239
+ /**
240
+ * Settle one send attempt under the row's claim. A confirmed message id
241
+ * is the only success: the part is sent, and a reply part's id becomes
242
+ * the row's reply id. Anything else leaves the part pending with its
243
+ * attempt counted, its error named, its next attempt scheduled, and —
244
+ * when the answer was lost rather than refused — its uncertainty counted.
245
+ * A lapsed claim settles nothing, so a reclaimer's outcome stands.
246
+ */
247
+ settleReplyPart(conversation: number, ordinal: number, owner: string, outcome: {
248
+ ok: true;
249
+ messageId: string;
250
+ } | {
251
+ ok: false;
252
+ error: string;
253
+ uncertain: boolean;
254
+ retryAt: string;
255
+ }, now: Date): boolean;
256
+ /** A card whose proposal no longer waits (confirmed or dismissed from another surface, or gone) is moot: dropped with the reason, never sent. */
257
+ dropReplyPart(conversation: number, ordinal: number, owner: string, reason: string, now: Date): boolean;
258
+ /** The exact task/run bindings of one outbound message this bot sent: a
259
+ * plain fact names one, a digest part may name several, (v64) a confirmed
260
+ * result image names its exact task and run, the lead's reply names the
261
+ * task its turn was about, a card names its task, and (v79) a status
262
+ * message names the task it showed. */
263
+ messageBindings(binding: TelegramBinding, messageId: string): {
264
+ taskId: string | null;
265
+ taskRef: number | null;
266
+ run: number | null;
267
+ project: string | null;
268
+ }[];
269
+ createProposalAction(action: {
270
+ token: string;
271
+ binding: number;
272
+ proposal: number;
273
+ phase: TelegramProposalAction["phase"];
274
+ chatId: string;
275
+ messageId?: string;
276
+ ttlMs?: number;
277
+ }, now: Date): void;
278
+ proposalAction(token: string): TelegramProposalAction | null;
279
+ /** Consume once. False means somebody already did, or it expired. */
280
+ consumeProposalAction(token: string, now: Date): boolean;
281
+ /** Kill every live token on a proposal — after it resolves, or when a challenge is cancelled. */
282
+ consumeProposalActions(proposal: number, now: Date, phases?: readonly TelegramProposalAction["phase"][]): void;
283
+ /** Stamp the message a card's keyboard actually landed on. */
284
+ placeProposalActions(tokens: readonly string[], messageId: string): void;
285
+ destination(binding: TelegramBinding): string;
286
+ /** A progress card belongs to one builder result, including its root review.
287
+ * Do not combine planners, correction children, or separate build attempts. */
288
+ progressRun(row: Pick<Notification, "taskRef" | "run">): Run | null;
289
+ /** Reuse a confirmed message in this exact destination. A mixed digest,
290
+ * decision, image, or message naming another attempt is never editable. The
291
+ * outbound history survives a restart even if the final receipt was lost. */
292
+ progressMessage(binding: TelegramBinding, result: Run): string | null;
293
+ /** The existing outbox, with a separate receipt for this exact pairing. */
294
+ claimDeliveries(binding: TelegramBinding, owner: string, ttlMs: number, now: Date, only?: "all" | "urgent"): TelegramDelivery[];
295
+ /** Synchronous final fence, called AFTER each asynchronous enrollment read. */
296
+ deliveryProblem(row: TelegramDelivery, binding: TelegramBinding, owner: string, projects: readonly string[], now: Date, batch?: readonly number[]): string | null;
297
+ claimHeld(row: TelegramDelivery, owner: string, now: Date): boolean;
298
+ /** A confirmed network message remains history even if authority changed
299
+ * while it was in flight. It only binds the OLD destination, never new work. */
300
+ recordMessage(row: TelegramDelivery, binding: TelegramBinding, messageId: string, now: Date): void;
301
+ /** The newest confirmed message this notification became at one destination, if any. */
302
+ messageOf(notification: number, destination: string): string | null;
303
+ finalizeDelivery(row: TelegramDelivery, binding: TelegramBinding, owner: string, outcome: {
304
+ ok: true;
305
+ receipt: string | null;
306
+ } | {
307
+ ok: false;
308
+ error: string;
309
+ retryAt?: string;
310
+ }, now: Date): boolean;
311
+ /** The notifications this pairing holds, each with its delivery state there. */
312
+ deliveries(binding: TelegramBinding): NotificationReceipt[];
313
+ /** The digest cadence: null everyMs = off. */
314
+ digest(): TelegramDigest;
315
+ /** Set (or clear, with null) the cadence. Turning it on starts the
316
+ * window NOW — nothing held before the choice is dumped at once. */
317
+ setDigest(everyMs: number | null, by: string, now: Date): void;
318
+ markDigestSent(now: Date): void;
319
+ }