toolroll 0.9.52 → 0.9.54

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 (187) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +55 -80
  3. package/dist/agentconfig.d.ts +11 -36
  4. package/dist/agentconfig.js +3 -62
  5. package/dist/approval-policy.d.ts +1 -1
  6. package/dist/approval-policy.js +1 -1
  7. package/dist/board.d.ts +3 -19
  8. package/dist/board.js +4 -42
  9. package/dist/browser/workspace.js +18 -18
  10. package/dist/browser-workspace.d.ts +2 -1
  11. package/dist/builder.d.ts +9 -40
  12. package/dist/builder.js +50 -257
  13. package/dist/chat-actions.js +3 -14
  14. package/dist/chat-ask.js +8 -8
  15. package/dist/chat-controls.d.ts +0 -4
  16. package/dist/chat-controls.js +0 -1
  17. package/dist/chat-core.d.ts +171 -0
  18. package/dist/chat-core.js +288 -0
  19. package/dist/chat-decide.d.ts +10 -8
  20. package/dist/chat-decide.js +27 -24
  21. package/dist/chat-delivery-state.d.ts +51 -75
  22. package/dist/chat-delivery-state.js +140 -170
  23. package/dist/chat-delivery.d.ts +21 -4
  24. package/dist/chat-delivery.js +313 -195
  25. package/dist/chat-flow.d.ts +29 -1
  26. package/dist/chat-flow.js +24 -50
  27. package/dist/chat-messages.d.ts +319 -0
  28. package/dist/chat-messages.js +841 -0
  29. package/dist/chat-migration.d.ts +30 -0
  30. package/dist/chat-migration.js +802 -0
  31. package/dist/chat-quiet.d.ts +3 -0
  32. package/dist/chat-quiet.js +7 -2
  33. package/dist/chat-task-actions.d.ts +1 -1
  34. package/dist/chat-task-actions.js +0 -2
  35. package/dist/claim.d.ts +79 -244
  36. package/dist/claim.js +688 -1023
  37. package/dist/cli.d.ts +2 -0
  38. package/dist/cli.js +10 -2
  39. package/dist/contracts/chat-content.d.ts +71 -2
  40. package/dist/contracts/chat-content.js +22 -3
  41. package/dist/contracts/chat-tables.d.ts +11 -4
  42. package/dist/contracts/chat-tables.js +165 -87
  43. package/dist/contracts/cli.d.ts +0 -13
  44. package/dist/contracts/cli.js +0 -2
  45. package/dist/contracts/console-api.d.ts +5 -10
  46. package/dist/contracts/console-api.js +4 -10
  47. package/dist/contracts/discord-callback.js +2 -1
  48. package/dist/contracts/flow.d.ts +50 -0
  49. package/dist/contracts/flow.js +26 -1
  50. package/dist/contracts/lead-tools.d.ts +0 -13
  51. package/dist/contracts/lead-tools.js +3 -3
  52. package/dist/contracts/slack-callback.js +3 -2
  53. package/dist/contracts/store-json.d.ts +1 -29
  54. package/dist/contracts/store-json.js +4 -17
  55. package/dist/contracts/teams-callback.js +2 -1
  56. package/dist/converse.d.ts +1 -11
  57. package/dist/converse.js +6 -38
  58. package/dist/coordinator.d.ts +0 -1
  59. package/dist/coordinator.js +3 -5
  60. package/dist/demo.js +16 -26
  61. package/dist/desktop-update-gate.js +0 -1
  62. package/dist/desktop-update.js +1 -3
  63. package/dist/discord-chat.js +19 -38
  64. package/dist/discord-settings.js +4 -13
  65. package/dist/discord.js +4 -14
  66. package/dist/dispatch.d.ts +1 -1
  67. package/dist/dispatch.js +1 -1
  68. package/dist/dispose.d.ts +2 -3
  69. package/dist/dispose.js +17 -92
  70. package/dist/envelope.js +0 -3
  71. package/dist/exec.d.ts +0 -64
  72. package/dist/exec.js +3 -356
  73. package/dist/exhaustion.d.ts +22 -76
  74. package/dist/exhaustion.js +14 -140
  75. package/dist/export.js +8 -8
  76. package/dist/flow-engine.js +1 -1
  77. package/dist/flow-schedule.d.ts +154 -0
  78. package/dist/flow-schedule.js +321 -0
  79. package/dist/flow-share.js +1 -1
  80. package/dist/flow-triggers.d.ts +6 -1
  81. package/dist/flow-triggers.js +65 -10
  82. package/dist/flows-cli.js +8 -4
  83. package/dist/flows-ui.js +2 -0
  84. package/dist/flows.js +1 -1
  85. package/dist/guides.js +9 -29
  86. package/dist/invoke.d.ts +1 -24
  87. package/dist/invoke.js +17 -259
  88. package/dist/lead-status.js +2 -4
  89. package/dist/lead-voice.js +1 -1
  90. package/dist/mate-doors.d.ts +1 -2
  91. package/dist/mate-doors.js +12 -25
  92. package/dist/mate-tools.d.ts +2 -2
  93. package/dist/mate-tools.js +13 -24
  94. package/dist/modes.d.ts +19 -8
  95. package/dist/modes.js +24 -14
  96. package/dist/notification-rows.d.ts +30 -0
  97. package/dist/notification-rows.js +102 -0
  98. package/dist/operate-remote-arguments.js +2 -6
  99. package/dist/operate-remote.js +6 -7
  100. package/dist/operate.d.ts +0 -6
  101. package/dist/operate.js +226 -2248
  102. package/dist/phase-routing.d.ts +13 -23
  103. package/dist/phase-routing.js +13 -35
  104. package/dist/plan-auto.js +2 -2
  105. package/dist/plane-review.js +5 -11
  106. package/dist/planner-source.js +2 -1
  107. package/dist/policy.d.ts +1 -4
  108. package/dist/policy.js +0 -8
  109. package/dist/process-recovery-eligibility.js +2 -3
  110. package/dist/project-delete.js +24 -41
  111. package/dist/proposal.d.ts +4 -24
  112. package/dist/proposal.js +3 -88
  113. package/dist/provider-auth.d.ts +3 -3
  114. package/dist/provider-auth.js +4 -6
  115. package/dist/provider.d.ts +11 -25
  116. package/dist/provider.js +4 -36
  117. package/dist/push.js +1 -1
  118. package/dist/recipe-ui.js +6 -6
  119. package/dist/recipes.d.ts +3 -3
  120. package/dist/recipes.js +27 -23
  121. package/dist/result-shots.js +2 -2
  122. package/dist/retention.js +1 -1
  123. package/dist/run-closeout.d.ts +1 -1
  124. package/dist/run-closeout.js +1 -4
  125. package/dist/scope.d.ts +11 -65
  126. package/dist/scope.js +17 -82
  127. package/dist/serve.js +15 -24
  128. package/dist/server/chat.js +7 -30
  129. package/dist/server/flows.js +11 -10
  130. package/dist/server/guards.js +0 -1
  131. package/dist/server/pages-assets.js +1 -1
  132. package/dist/server/pages.js +0 -2
  133. package/dist/server/people-tokens.js +0 -4
  134. package/dist/server/route-table.d.ts +1 -1
  135. package/dist/server/route-table.js +2 -17
  136. package/dist/server/settings.js +6 -12
  137. package/dist/server/shared.d.ts +28 -212
  138. package/dist/server/shared.js +71 -780
  139. package/dist/server/task-views.js +9 -76
  140. package/dist/server/tasks.d.ts +1 -50
  141. package/dist/server/tasks.js +36 -946
  142. package/dist/setup-guide.d.ts +2 -2
  143. package/dist/slack-api.d.ts +5 -1
  144. package/dist/slack-api.js +2 -2
  145. package/dist/slack-chat.d.ts +9 -6
  146. package/dist/slack-chat.js +29 -53
  147. package/dist/slack-settings.js +6 -15
  148. package/dist/slack.js +6 -16
  149. package/dist/store.d.ts +200 -1702
  150. package/dist/store.js +811 -5439
  151. package/dist/surface.js +10 -24
  152. package/dist/task-composer.d.ts +1 -1
  153. package/dist/task-composer.js +1 -1
  154. package/dist/task-control.d.ts +3 -10
  155. package/dist/task-control.js +2 -7
  156. package/dist/teammate-desk.js +1 -1
  157. package/dist/teammate-question.js +12 -12
  158. package/dist/teammates-ui.js +1 -1
  159. package/dist/teams-chat.js +12 -9
  160. package/dist/teams-settings.js +2 -2
  161. package/dist/teams.js +4 -5
  162. package/dist/telegram-flow.js +5 -5
  163. package/dist/telegram-mate.d.ts +0 -3
  164. package/dist/telegram-mate.js +0 -1
  165. package/dist/telegram.js +4 -4
  166. package/dist/templates.d.ts +11 -7
  167. package/dist/templates.js +12 -9
  168. package/dist/toolroll-update.d.ts +20 -0
  169. package/dist/toolroll-update.js +104 -5
  170. package/dist/work-index.js +2 -6
  171. package/dist/workspace-revision.js +3 -1
  172. package/dist/workspace-ui.js +1 -1
  173. package/package.json +1 -1
  174. package/scripts/recovery-canary.mjs +3 -3
  175. package/dist/contest.d.ts +0 -318
  176. package/dist/contest.js +0 -753
  177. package/dist/held.d.ts +0 -150
  178. package/dist/held.js +0 -799
  179. package/dist/liveness.d.ts +0 -30
  180. package/dist/liveness.js +0 -42
  181. package/dist/pricing.d.ts +0 -45
  182. package/dist/pricing.js +0 -77
  183. package/dist/routine.d.ts +0 -228
  184. package/dist/routine.js +0 -764
  185. package/dist/slack-state.d.ts +0 -13
  186. package/dist/slack-state.js +0 -10
  187. package/dist/supervisor.mjs +0 -297
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
15
15
  } | null;
16
16
  /** Browser installation may write only this known file inside its project. */
17
17
  export declare function previewProjectInstructions(repo: string): {
18
- content?: never;
19
18
  ok: false;
20
19
  message: string;
21
20
  plan?: never;
21
+ content?: never;
22
22
  fingerprint?: never;
23
23
  installed?: never;
24
24
  } | {
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
30
30
  installed: boolean;
31
31
  };
32
32
  export declare function addProjectInstructions(repo: string, fingerprint: string): {
33
- content?: never;
34
33
  ok: false;
35
34
  message: string;
36
35
  plan?: never;
36
+ content?: never;
37
37
  fingerprint?: never;
38
38
  installed?: never;
39
39
  } | import("./skills.js").InstallResult;
@@ -1,5 +1,9 @@
1
1
  import { ChatDeliveryError } from "./chat-delivery-state.js";
2
- import { type SlackIdentity } from "./slack-state.js";
2
+ import { type ChatIdentity } from "./chat-delivery-state.js";
3
+ /** A Slack app installation: the chat identity, with the workspace (team) it belongs to. */
4
+ export type SlackIdentity = ChatIdentity & {
5
+ team: string;
6
+ };
3
7
  export type SlackCredentials = SlackIdentity & {
4
8
  appToken: string;
5
9
  botToken: string;
package/dist/slack-api.js CHANGED
@@ -2,7 +2,7 @@ import { ChatDeliveryError } from "./chat-delivery-state.js";
2
2
  import { chmodSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { randomBytes } from "node:crypto";
5
- import { slackHash } from "./slack-state.js";
5
+ import { chatHash } from "./chat-delivery-state.js";
6
6
  export class SlackError extends ChatDeliveryError {
7
7
  constructor(code, retryMs = 5_000, uncertain = false) {
8
8
  super(code, retryMs, uncertain);
@@ -128,7 +128,7 @@ export async function checkSlackCredentials(appToken, botToken, fetcher = fetch)
128
128
  app: String(detail.app_id),
129
129
  bot: String(auth.user_id),
130
130
  workspace: typeof auth.team === "string" ? auth.team : "Slack",
131
- installation: slackHash(`${auth.team_id}:${detail.app_id}:${auth.user_id}`),
131
+ installation: chatHash(`${auth.team_id}:${detail.app_id}:${auth.user_id}`),
132
132
  };
133
133
  }
134
134
  export function object(value) {
@@ -1,6 +1,5 @@
1
- import { type DoorOptions } from "./mate-doors.js";
2
- import { SlackState, type SlackIdentity, type SlackBinding, type SlackEvent } from "./slack-state.js";
3
- import { uploadSlackBytes, type SlackApi } from "./slack-api.js";
1
+ import { ChatState, type ChatBinding, type ChatContent, type ChatEvent } from "./chat-delivery-state.js";
2
+ import { uploadSlackBytes, type SlackApi, type SlackIdentity } from "./slack-api.js";
4
3
  import { type Store } from "./store.js";
5
4
  import type { SubscriptionMateRunner } from "./subscription-chat.js";
6
5
  export type SlackChatOptions = {
@@ -13,15 +12,19 @@ export type SlackChatOptions = {
13
12
  current: () => boolean;
14
13
  origin: () => string | null;
15
14
  subscriptionRunner?: SubscriptionMateRunner;
16
- held?: DoorOptions["held"];
17
15
  canNotify?: () => boolean;
18
16
  clock?: () => Date;
19
17
  upload?: typeof uploadSlackBytes;
20
18
  };
21
19
  /** Synchronous receipt before ACK. Never stores response_url, raw tokens, or a pairing code. */
22
- export declare function receiveSlack(state: SlackState, identity: SlackIdentity, type: string, raw: unknown, now: Date): boolean;
20
+ export declare function receiveSlack(state: ChatState, identity: SlackIdentity, type: string, raw: unknown, now: Date): boolean;
23
21
  export declare const processSlackEvent: (options: SlackChatOptions) => Promise<boolean>;
24
- export declare const applySlackAction: (options: SlackChatOptions, event: SlackEvent, binding: SlackBinding, repos: readonly string[]) => void;
22
+ export declare const applySlackAction: (options: SlackChatOptions, event: ChatEvent, binding: ChatBinding, repos: readonly string[]) => {
23
+ merge: import("./chat-decide.js").ChatMerge;
24
+ part: number;
25
+ shown: string;
26
+ content: ChatContent;
27
+ } | null;
25
28
  export declare const planSlackNotifications: (options: SlackChatOptions) => Promise<void>;
26
29
  export declare const planSlackRooms: (options: SlackChatOptions) => Promise<void>;
27
30
  export declare function slackBlocks(text: string, buttons?: Record<string, unknown>[]): Record<string, unknown>[];
@@ -9,9 +9,7 @@ import { processChatEvent, applyChatAction, planChatNotifications, channelAccess
9
9
  import { proposalPreview, proposalLink, proposalOutcomeText, armedCardText, armedYesLabel, } from "./chat-channel.js";
10
10
  import { MATE_MESSAGE_MAX_CHARS } from "./mate.js";
11
11
  import { renderReply } from "./reply-shape.js";
12
- import {} from "./mate-doors.js";
13
- import { SlackState, slackHash, } from "./slack-state.js";
14
- import { ChatDeliveryError, partContent } from "./chat-delivery-state.js";
12
+ import { ChatDeliveryError, ChatState, chatHash, partContent } from "./chat-delivery-state.js";
15
13
  import { readChatPart } from "./contracts/chat-content.js";
16
14
  import { readSlackBlockActions, readSlackButton, SLACK_LINK_ACTION } from "./contracts/slack-callback.js";
17
15
  import { object, slackId, slackTs, slackMember, SlackError, uploadSlackBytes, } from "./slack-api.js";
@@ -65,12 +63,12 @@ export function receiveSlack(state, identity, type, raw, now) {
65
63
  const match = /^pair ([a-f0-9]{32})$/.exec(event.text.trim());
66
64
  kind = match ? "pair" : "message";
67
65
  payload = match
68
- ? { hash: slackHash(match[1]) }
66
+ ? { hash: chatHash(match[1]) }
69
67
  : {
70
68
  text: event.text.slice(0, MATE_MESSAGE_MAX_CHARS + 1),
71
69
  originalLength: event.text.length,
72
70
  };
73
- id = slackHash(`${identity.installation}:event:${body.event_id}`);
71
+ id = chatHash(`${identity.installation}:event:${body.event_id}`);
74
72
  }
75
73
  else if (type === "interactive" && body.type === "block_actions") {
76
74
  const interaction = readSlackBlockActions(body);
@@ -89,7 +87,7 @@ export function receiveSlack(state, identity, type, raw, now) {
89
87
  const button = readSlackButton(interaction.value);
90
88
  payload = button.ok ? { token: button.value.value } : { problem: button.issues.map(issue => issue.line).join("; ") };
91
89
  const tapped = actions[0];
92
- id = slackHash(`${identity.installation}:action:${member}:${ts}:${tapped?.value ?? ""}:${tapped?.action_ts ?? ""}`);
90
+ id = chatHash(`${identity.installation}:action:${member}:${ts}:${tapped?.value ?? ""}:${tapped?.action_ts ?? ""}`);
93
91
  }
94
92
  else
95
93
  return false;
@@ -132,7 +130,7 @@ export function receiveSlack(state, identity, type, raw, now) {
132
130
  }
133
131
  const delivery = (options) => ({
134
132
  ...options,
135
- state: new SlackState(options.store),
133
+ state: new ChatState(options.store, "slack"),
136
134
  label: "Slack",
137
135
  member: (member, channel) => slackMember(options.api, options.identity, member, channel),
138
136
  // A 👍 on the owner's message where the app has reactions:write; Slack shows bots no typing indicator.
@@ -202,26 +200,20 @@ function linkButton(origin, link) {
202
200
  }
203
201
  }
204
202
  export async function deliverSlackPart(options) {
205
- const state = new SlackState(options.store), { store, identity } = options, now = nowOf(options);
203
+ const state = new ChatState(options.store, "slack"), { store, identity } = options, now = nowOf(options);
206
204
  if (!state.owns(identity.installation, options.owner, now) ||
207
205
  !options.current())
208
206
  return false;
209
- const row = state.db
210
- .prepare("SELECT p.* FROM slack_part p JOIN slack_event e ON e.id=p.event WHERE e.installation=? AND p.state='pending' AND (e.kind!='notice' OR ?=1) AND (p.next_at IS NULL OR p.next_at<=?) ORDER BY p.id LIMIT 1")
211
- .get(identity.installation, options.canNotify?.() === false ? 0 : 1, now.toISOString());
207
+ const row = state.nextPart(identity.installation, options.canNotify?.() !== false, now);
212
208
  if (!row)
213
209
  return false;
214
210
  const event = state.event(row.event), binding = event.binding === null ? null : state.bindingById(event.binding);
215
211
  if (!binding) {
216
- state.db
217
- .prepare("UPDATE slack_part SET state='dropped',problem='Chat access changed' WHERE id=?")
218
- .run(row.id);
212
+ state.dropPart(row.id, "Chat access changed");
219
213
  return true;
220
214
  }
221
215
  if (new Date(row.created).getTime() + 86_400_000 < now.getTime()) {
222
- state.db
223
- .prepare("UPDATE slack_part SET state='dropped',problem='Delivery expired; open the saved chat to recover it' WHERE id=?")
224
- .run(row.id);
216
+ state.dropPart(row.id, "Delivery expired; open the saved chat to recover it");
225
217
  return true;
226
218
  }
227
219
  try {
@@ -236,15 +228,15 @@ export async function deliverSlackPart(options) {
236
228
  !repos.includes(store.lookupRef(content.task)?.repo ?? ""))
237
229
  throw new SlackError("Connected projects changed");
238
230
  // A screenshot sent with a result goes in that result's thread once its message is placed, and waits until then.
239
- const result = content.shot?.follows == null ? undefined : state.db.prepare("SELECT message,state,next_at FROM slack_part WHERE id=?").get(content.shot.follows);
240
- if (result !== undefined && !slackTs(result["message"]) && result["state"] === "pending") {
241
- state.db.prepare("UPDATE slack_part SET next_at=? WHERE id=?").run(shotWaitsUntil(result["next_at"], now), row.id);
231
+ const result = content.shot?.follows == null ? null : state.part(content.shot.follows);
232
+ if (result !== null && !slackTs(result.message) && result.state === "pending") {
233
+ state.holdPart(row.id, shotWaitsUntil(result.next_at, now));
242
234
  return true;
243
235
  }
244
- const follows = result?.["message"];
236
+ const follows = result?.message;
245
237
  const thread = slackTs(follows) ? String(follows) : event.thread;
246
238
  if (content.image && content.shot && resultShotsPruned(options.evidenceRoot, content.image.run)) {
247
- state.db.prepare("UPDATE slack_part SET state='dropped',problem='Removed by retention' WHERE id=?").run(row.id);
239
+ state.dropPart(row.id, "Removed by retention");
248
240
  return true;
249
241
  }
250
242
  if (content.image) {
@@ -263,17 +255,13 @@ export async function deliverSlackPart(options) {
263
255
  typeof allocated.upload_url !== "string")
264
256
  throw new SlackError("Slack did not identify the upload");
265
257
  file = allocated.file_id;
266
- state.db
267
- .prepare("UPDATE slack_part SET file=? WHERE id=?")
268
- .run(file, row.id);
258
+ state.setPartFile(row.id, file);
269
259
  await access(options, binding, session?.ceilingDigest);
270
260
  const fresh = verifyResultImage(store, options.evidenceRoot, repos, content.image);
271
261
  if (!fresh.ok)
272
262
  throw new SlackError("Screenshot evidence changed before upload");
273
263
  await (options.upload ?? uploadSlackBytes)(allocated.upload_url, fresh.bytes);
274
- state.db
275
- .prepare("UPDATE slack_part SET uploaded=1 WHERE id=?")
276
- .run(row.id);
264
+ state.setPartFile(row.id, null, true);
277
265
  }
278
266
  await access(options, binding, session?.ceilingDigest);
279
267
  if (!verifyResultImage(store, options.evidenceRoot, repos, content.image)
@@ -289,9 +277,7 @@ export async function deliverSlackPart(options) {
289
277
  .map(object)
290
278
  .find((receipt) => (receipt.thread_ts ?? "") === thread && slackTs(receipt.ts));
291
279
  if (found) {
292
- state.db
293
- .prepare("UPDATE slack_part SET state='sent',message=?,problem=NULL WHERE id=?")
294
- .run(String(found.ts), row.id);
280
+ state.partSent(row.id, String(found.ts), nowOf(options), false);
295
281
  return true;
296
282
  }
297
283
  }
@@ -309,9 +295,7 @@ export async function deliverSlackPart(options) {
309
295
  if (!Array.isArray(completed.files) ||
310
296
  !completed.files.some((value) => object(value).id === file))
311
297
  throw new SlackError("Slack did not confirm the file identity", 15_000, true);
312
- state.db
313
- .prepare("UPDATE slack_part SET state='sent',problem=NULL WHERE id=?")
314
- .run(row.id);
298
+ state.partSent(row.id, null, nowOf(options), false);
315
299
  return true;
316
300
  }
317
301
  }
@@ -328,9 +312,7 @@ export async function deliverSlackPart(options) {
328
312
  ? armedCardText(proposal, preview.text)
329
313
  : preview.text.replace("\n\nConfirm or Dismiss below. Nothing changes until you confirm.", "");
330
314
  if (preview.buttons && preview.text.length <= 10_000) {
331
- const tokens = state.db
332
- .prepare("SELECT token,phase FROM slack_action WHERE part=? AND consumed IS NULL AND expires>? ORDER BY rowid")
333
- .all(row.id, now.toISOString());
315
+ const tokens = state.partTokens(row.id, now);
334
316
  buttons = tokens.map((action) => ({
335
317
  type: "button",
336
318
  text: {
@@ -399,6 +381,10 @@ export async function deliverSlackPart(options) {
399
381
  value: one.token,
400
382
  }))
401
383
  : []),
384
+ // A result's, plan's, failure's or pull request's own buttons (chat-decide.ts): its acts, then its links.
385
+ ...(content.decide?.rows.flat() ?? []).flatMap((one, index) => "token" in one
386
+ ? [{ type: "button", text: { type: "plain_text", text: one.label }, action_id: `toolroll_decide_${index}`, value: one.token, ...(index === 0 ? { style: "primary" } : {}) }]
387
+ : linkButton(options.origin(), one.link).map(button => ({ ...button, action_id: `toolroll_link_${index + 10}` }))),
402
388
  ...linkButton(options.origin(), content.link),
403
389
  // [Look first] beside [Merge] or [Accept and finish]: each button's own action id.
404
390
  ...(content.also ?? []).flatMap((one, index) => linkButton(options.origin(), one).map(button => ({ ...button, action_id: `toolroll_link_${index + 2}` }))),
@@ -428,17 +414,13 @@ export async function deliverSlackPart(options) {
428
414
  });
429
415
  if (!slackTs(answer.ts) || (target && answer.ts !== target))
430
416
  throw new SlackError("Slack did not confirm the message identity", 15_000, true);
431
- state.db
432
- .prepare("UPDATE slack_part SET state='sent',message=?,attempts=attempts+1,problem=NULL,next_at=NULL WHERE id=?")
433
- .run(answer.ts, row.id);
434
- state.db
435
- .prepare("UPDATE slack_runtime SET problem=NULL WHERE installation=? AND owner=?")
436
- .run(identity.installation, options.owner);
417
+ state.partSent(row.id, String(answer.ts), nowOf(options));
418
+ state.setProblem(identity.installation, options.owner, null);
437
419
  return true;
438
420
  }
439
421
  catch (error) {
440
422
  if (error instanceof ChatDeliveryError && error.permanent) {
441
- state.prepare("UPDATE chat_part SET state='dropped',next_at=NULL,problem=? WHERE id=?").run(error.message, row.id);
423
+ state.dropPart(row.id, error.message);
442
424
  return true;
443
425
  }
444
426
  // No permission to upload files here: one plain line instead of the result's screenshots.
@@ -450,18 +432,12 @@ export async function deliverSlackPart(options) {
450
432
  const problem = error instanceof SlackError
451
433
  ? error
452
434
  : new SlackError("Slack delivery is waiting to retry", 15_000, true);
453
- state.db
454
- .prepare("UPDATE slack_runtime SET problem=? WHERE installation=? AND owner=?")
455
- .run(problem.message, identity.installation, options.owner);
435
+ state.setProblem(identity.installation, options.owner, problem.message);
456
436
  const until = new Date(nowOf(options).getTime() +
457
437
  Math.max(problem.retryMs, [5000, 15000, 60000, 300000][Math.min(row.attempts, 3)])).toISOString();
458
- state.db
459
- .prepare("UPDATE slack_part SET attempts=attempts+1,uncertain=uncertain+?,next_at=?,problem=? WHERE id=?")
460
- .run(problem.uncertain ? 1 : 0, until, problem.message, row.id);
438
+ state.partFailed(row.id, { problem: problem.message, until, uncertain: problem.uncertain });
461
439
  if (problem.code === "ratelimited")
462
- state.db
463
- .prepare("UPDATE slack_runtime SET retry_at=? WHERE installation=?")
464
- .run(until, identity.installation);
440
+ state.deferUntil(identity.installation, until);
465
441
  return true;
466
442
  }
467
443
  }
@@ -1,6 +1,6 @@
1
1
  import { loadPrimary } from "./webhooks.js";
2
2
  import { loadSlackCredentials } from "./slack-api.js";
3
- import { SlackState } from "./slack-state.js";
3
+ import { ChatState } from "./chat-delivery-state.js";
4
4
  const escape = (text) => text
5
5
  .replaceAll("&", "&amp;")
6
6
  .replaceAll("<", "&lt;")
@@ -8,15 +8,11 @@ const escape = (text) => text
8
8
  .replaceAll('"', "&quot;")
9
9
  .replaceAll("'", "&#39;");
10
10
  export function slackSettingsHtml(store, dir, csrf, options = {}) {
11
- const credentials = loadSlackCredentials(dir), state = new SlackState(store), bindings = credentials ? state.bindings(credentials.installation).filter(one => state.live(one)) : [], binding = options.who === undefined ? null : bindings.find(one => one.approver === options.who) ?? null, others = bindings.length - (binding === null ? 0 : 1);
12
- const runtime = credentials
13
- ? state.db
14
- .prepare("SELECT connected,problem,lease_until FROM slack_runtime WHERE installation=?")
15
- .get(credentials.installation)
16
- : null;
11
+ const credentials = loadSlackCredentials(dir), state = new ChatState(store, "slack"), bindings = credentials ? state.bindings(credentials.installation).filter(one => state.live(one)) : [], binding = options.who === undefined ? null : bindings.find(one => one.approver === options.who) ?? null, others = bindings.length - (binding === null ? 0 : 1);
12
+ const runtime = credentials ? state.runtime(credentials.installation) : null;
17
13
  const now = options.now ?? new Date(), live = runtime &&
18
- typeof runtime.lease_until === "string" &&
19
- runtime.lease_until > now.toISOString() &&
14
+ runtime.leaseUntil !== null &&
15
+ runtime.leaseUntil > now.toISOString() &&
20
16
  runtime.connected;
21
17
  const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">`;
22
18
  const password = '<label>Your Toolroll password<input style="min-height:44px" type="password" name="password" autocomplete="current-password" required></label>';
@@ -44,12 +40,7 @@ export function slackSettingsHtml(store, dir, csrf, options = {}) {
44
40
  '<a class="button-link" style="min-height:44px;white-space:nowrap" href="/settings/slack">Check connection</a>';
45
41
  }
46
42
  else if (binding && state.live(binding)) {
47
- const pending = Number(state.db
48
- .prepare("SELECT count(*) n FROM slack_part p JOIN slack_event e ON e.id=p.event WHERE e.binding=? AND p.state='pending'")
49
- .get(binding.id)?.n ?? 0);
50
- const failed = Number(state.db
51
- .prepare("SELECT count(*) n FROM slack_part p JOIN slack_event e ON e.id=p.event WHERE e.binding=? AND p.state='dropped'")
52
- .get(binding.id)?.n ?? 0);
43
+ const { pending, dropped: failed } = state.partCounts(binding.id);
53
44
  content +=
54
45
  `<p>Your Slack account is paired.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} paired too.` : ""}${pending ? ` ${pending} replies waiting to send.` : ""}${failed ? ` ${failed} replies could not be delivered. Open the saved chat to recover them.` : ""}</p>` +
55
46
  "<p>Try “What needs my attention?” or “Show the evidence for the latest result.”</p>" +
package/dist/slack.js CHANGED
@@ -3,7 +3,7 @@ import { randomBytes } from "node:crypto";
3
3
  import { setTimeout as sleep } from "node:timers/promises";
4
4
  import { SocketModeClient, LogLevel } from "@slack/socket-mode";
5
5
  import { loadSlackCredentials, slackApi } from "./slack-api.js";
6
- import { SlackState } from "./slack-state.js";
6
+ import { ChatState } from "./chat-delivery-state.js";
7
7
  import { receiveSlack, processSlackEvent, deliverSlackPart, planSlackNotifications, planSlackRooms, } from "./slack-chat.js";
8
8
  /** The bot and socket tokens must belong to the same app, including after reconnect. */
9
9
  export class ToolrollSlackSocket extends SocketModeClient {
@@ -29,7 +29,7 @@ export class ToolrollSlackSocket extends SocketModeClient {
29
29
  }
30
30
  }
31
31
  export async function followSlack(options) {
32
- const owner = randomBytes(16).toString("hex"), state = new SlackState(options.store);
32
+ const owner = randomBytes(16).toString("hex"), state = new ChatState(options.store, "slack");
33
33
  const pause = async (ms) => {
34
34
  try {
35
35
  await sleep(ms, undefined, { signal: options.signal });
@@ -55,9 +55,7 @@ export async function followSlack(options) {
55
55
  current.appToken === credentials.appToken &&
56
56
  state.owns(credentials.installation, owner));
57
57
  };
58
- const problem = (text) => state.db
59
- .prepare("UPDATE slack_runtime SET problem=? WHERE installation=? AND owner=?")
60
- .run(text, credentials.installation, owner);
58
+ const problem = (text) => state.setProblem(credentials.installation, owner, text);
61
59
  // SDK log output can include envelopes/tokens. Only fixed status phrases are saved.
62
60
  const client = new ToolrollSlackSocket({
63
61
  appToken: credentials.appToken,
@@ -93,9 +91,7 @@ export async function followSlack(options) {
93
91
  }, 10_000);
94
92
  client.on("connected", () => {
95
93
  connected = true;
96
- state.db
97
- .prepare("UPDATE slack_runtime SET connected=?,problem=NULL WHERE installation=? AND owner=?")
98
- .run(new Date().toISOString(), credentials.installation, owner);
94
+ state.setConnected(credentials.installation, owner, new Date());
99
95
  });
100
96
  client.on("disconnected", () => {
101
97
  connected = false;
@@ -131,11 +127,7 @@ export async function followSlack(options) {
131
127
  try {
132
128
  await client.start();
133
129
  while (same()) {
134
- const retry = state.db
135
- .prepare("SELECT retry_at FROM slack_runtime WHERE installation=?")
136
- .get(credentials.installation)?.retry_at;
137
- if (!connected ||
138
- (typeof retry === "string" && retry > new Date().toISOString())) {
130
+ if (!connected || state.retryAt(credentials.installation) > new Date().toISOString()) {
139
131
  await pause(1000);
140
132
  continue;
141
133
  }
@@ -158,9 +150,7 @@ export async function followSlack(options) {
158
150
  alive = false;
159
151
  options.signal.removeEventListener("abort", stop);
160
152
  await client.disconnect().catch(() => { });
161
- state.db
162
- .prepare("UPDATE slack_runtime SET owner=NULL,lease_until=NULL,connected=NULL WHERE installation=? AND owner=?")
163
- .run(credentials.installation, owner);
153
+ state.stop(credentials.installation, owner);
164
154
  }
165
155
  await pause(5000);
166
156
  }