toolroll 0.9.53 → 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.
- package/CHANGELOG.md +8 -0
- package/dist/chat-ask.js +8 -8
- package/dist/chat-core.d.ts +171 -0
- package/dist/chat-core.js +288 -0
- package/dist/chat-decide.d.ts +10 -8
- package/dist/chat-decide.js +26 -21
- package/dist/chat-delivery-state.d.ts +51 -75
- package/dist/chat-delivery-state.js +140 -170
- package/dist/chat-delivery.d.ts +21 -2
- package/dist/chat-delivery.js +313 -194
- package/dist/chat-flow.d.ts +29 -1
- package/dist/chat-flow.js +24 -50
- package/dist/chat-messages.d.ts +319 -0
- package/dist/chat-messages.js +841 -0
- package/dist/chat-migration.d.ts +30 -0
- package/dist/chat-migration.js +802 -0
- package/dist/chat-quiet.d.ts +3 -0
- package/dist/chat-quiet.js +5 -0
- package/dist/contracts/chat-content.d.ts +71 -2
- package/dist/contracts/chat-content.js +22 -3
- package/dist/contracts/chat-tables.d.ts +11 -4
- package/dist/contracts/chat-tables.js +165 -87
- package/dist/contracts/discord-callback.js +2 -1
- package/dist/contracts/slack-callback.js +3 -2
- package/dist/contracts/store-json.d.ts +1 -5
- package/dist/contracts/store-json.js +1 -4
- package/dist/contracts/teams-callback.js +2 -1
- package/dist/discord-chat.js +19 -38
- package/dist/discord-settings.js +4 -13
- package/dist/discord.js +4 -14
- package/dist/export.js +4 -4
- package/dist/mate-tools.js +3 -2
- package/dist/modes.d.ts +13 -1
- package/dist/modes.js +17 -1
- package/dist/notification-rows.d.ts +30 -0
- package/dist/notification-rows.js +102 -0
- package/dist/operate.js +5 -2
- package/dist/plane-review.js +5 -11
- package/dist/project-delete.js +21 -26
- package/dist/result-shots.js +2 -2
- package/dist/retention.js +1 -1
- package/dist/server/settings.js +5 -4
- package/dist/slack-api.d.ts +5 -1
- package/dist/slack-api.js +2 -2
- package/dist/slack-chat.d.ts +9 -4
- package/dist/slack-chat.js +29 -52
- package/dist/slack-settings.js +6 -15
- package/dist/slack.js +6 -16
- package/dist/store.d.ts +97 -364
- package/dist/store.js +207 -1531
- package/dist/teammate-question.js +12 -12
- package/dist/teams-chat.js +12 -9
- package/dist/teams-settings.js +2 -2
- package/dist/teams.js +4 -5
- package/dist/telegram-flow.js +5 -5
- package/dist/telegram.js +4 -4
- package/dist/toolroll-update.d.ts +10 -0
- package/dist/toolroll-update.js +50 -3
- package/dist/workspace-revision.js +3 -1
- package/package.json +1 -1
- package/dist/slack-state.d.ts +0 -13
- package/dist/slack-state.js +0 -10
package/dist/retention.js
CHANGED
|
@@ -280,7 +280,7 @@ export function sweepRetention(store, evidenceRoot, now, actor = "worker") {
|
|
|
280
280
|
}
|
|
281
281
|
if (periods.notifications !== null) {
|
|
282
282
|
const deliveries = db.prepare("DELETE FROM notification_delivery WHERE notification = ?");
|
|
283
|
-
const outbound = db.prepare("DELETE FROM
|
|
283
|
+
const outbound = db.prepare("DELETE FROM chat_message_ref WHERE notification = ?");
|
|
284
284
|
const pushes = db.prepare("DELETE FROM push_delivery WHERE notification = ?");
|
|
285
285
|
const notification = db.prepare("DELETE FROM notification WHERE id = ?");
|
|
286
286
|
for (const one of plan.items.notifications) {
|
package/dist/server/settings.js
CHANGED
|
@@ -66,7 +66,7 @@ import { startConnect } from "../mcp-connect.js";
|
|
|
66
66
|
import { decideProposal, listProposals, memoryStatus } from "../memory-pass.js";
|
|
67
67
|
import { checkModels, isNewModel, modelOptions, modelWords, RUNTIME_TOOLS, runtimeStates, seenModels, setWatch, updateRuntime, watchState } from "../model-catalog.js";
|
|
68
68
|
import { modelsHtml, modelsScript } from "../models-ui.js";
|
|
69
|
-
import { MODE_MAX_DAYS, modeDigestOf, modeTermsJson, modeWords, presetTerms } from "../modes.js";
|
|
69
|
+
import { CHAT_APPROVE_ALL, MODE_MAX_DAYS, modeDigestOf, modeTermsJson, modeWords, presetTerms } from "../modes.js";
|
|
70
70
|
import { monitoringChange, readMonitoring, saveMonitoring } from "../monitoring-settings.js";
|
|
71
71
|
import { monitoringHtml, signingSecretHtml } from "../monitoring-ui.js";
|
|
72
72
|
import { openRouterPickerScript } from "../openrouter-models.js";
|
|
@@ -96,7 +96,6 @@ import { addProjectInstructions, ASSISTANTS, detectPreparation, modelChoices, pr
|
|
|
96
96
|
import { skillsHtml, skillsScript } from "../skills-ui.js";
|
|
97
97
|
import { checkSlackCredentials, clearSlackCredentials, loadSlackCredentials, saveSlackCredentials, SLACK_MANIFEST, SlackError } from "../slack-api.js";
|
|
98
98
|
import { slackSettingsHtml } from "../slack-settings.js";
|
|
99
|
-
import { SlackState } from "../slack-state.js";
|
|
100
99
|
import { spendCsv, spendHtml } from "../spend-ui.js";
|
|
101
100
|
import { budgetStates, monthNamed, spendItems, usd as spendUsd, teammateNames as teammateNamesOf } from "../spend.js";
|
|
102
101
|
import { removeSsoSettings, saveSsoSettings, SSO_CALLBACK, ssoChangeWords } from "../sso-settings.js";
|
|
@@ -1599,7 +1598,7 @@ export function createSettingsHandlers(runtime) {
|
|
|
1599
1598
|
const body = readForm(posted, CONSOLE_FORMS.slack);
|
|
1600
1599
|
if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
|
|
1601
1600
|
return refuse(response, who, 403, "An installation approver can connect Slack.", "/settings");
|
|
1602
|
-
const dir = options.configDir, state = new
|
|
1601
|
+
const dir = options.configDir, state = new ChatState(store, "slack"), action = url.pathname.split("/").at(-1);
|
|
1603
1602
|
const show = (problem, status = 400) => sendScreen(response, status, screen("Slack", slackSettingsHtml(store, dir, who.session.csrf, { problem, who: who.name }), { chrome: chromeFor(projectOf(who, request) ?? null, "settings") }));
|
|
1604
1603
|
if (["password", "app-token", "bot-token"].some(key => body.getAll(key).length > 1))
|
|
1605
1604
|
return show("Submit one value for each field.");
|
|
@@ -2050,8 +2049,10 @@ export function createSettingsHandlers(runtime) {
|
|
|
2050
2049
|
// attempt cap it carries is meaningless without it.
|
|
2051
2050
|
repairAuto: false,
|
|
2052
2051
|
repairMaxAttempts: 0,
|
|
2053
|
-
// Approving from the paired chat: only the explicit choice grants it
|
|
2052
|
+
// Approving from the paired chat: only the explicit choice grants it, naming every chat app it covers (a grant
|
|
2053
|
+
// signed before it named them stays Telegram only).
|
|
2054
2054
|
chatApprove: body.get("chat-approve") === "1",
|
|
2055
|
+
...(body.get("chat-approve") === "1" ? { chatApproveChats: CHAT_APPROVE_ALL } : {}),
|
|
2055
2056
|
publication: body.get("publication") === "automerge" ? "automerge" : "notify",
|
|
2056
2057
|
};
|
|
2057
2058
|
if (["review-auto", "review-retry-auto", "repair-auto"].some(field => body.get(field) === "1")) {
|
package/dist/slack-api.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { ChatDeliveryError } from "./chat-delivery-state.js";
|
|
2
|
-
import { type
|
|
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 {
|
|
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:
|
|
131
|
+
installation: chatHash(`${auth.team_id}:${detail.app_id}:${auth.user_id}`),
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
134
|
export function object(value) {
|
package/dist/slack-chat.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
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";
|
|
3
3
|
import { type Store } from "./store.js";
|
|
4
4
|
import type { SubscriptionMateRunner } from "./subscription-chat.js";
|
|
5
5
|
export type SlackChatOptions = {
|
|
@@ -17,9 +17,14 @@ export type SlackChatOptions = {
|
|
|
17
17
|
upload?: typeof uploadSlackBytes;
|
|
18
18
|
};
|
|
19
19
|
/** Synchronous receipt before ACK. Never stores response_url, raw tokens, or a pairing code. */
|
|
20
|
-
export declare function receiveSlack(state:
|
|
20
|
+
export declare function receiveSlack(state: ChatState, identity: SlackIdentity, type: string, raw: unknown, now: Date): boolean;
|
|
21
21
|
export declare const processSlackEvent: (options: SlackChatOptions) => Promise<boolean>;
|
|
22
|
-
export declare const applySlackAction: (options: SlackChatOptions, event:
|
|
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;
|
|
23
28
|
export declare const planSlackNotifications: (options: SlackChatOptions) => Promise<void>;
|
|
24
29
|
export declare const planSlackRooms: (options: SlackChatOptions) => Promise<void>;
|
|
25
30
|
export declare function slackBlocks(text: string, buttons?: Record<string, unknown>[]): Record<string, unknown>[];
|
package/dist/slack-chat.js
CHANGED
|
@@ -9,8 +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 {
|
|
13
|
-
import { ChatDeliveryError, partContent } from "./chat-delivery-state.js";
|
|
12
|
+
import { ChatDeliveryError, ChatState, chatHash, partContent } from "./chat-delivery-state.js";
|
|
14
13
|
import { readChatPart } from "./contracts/chat-content.js";
|
|
15
14
|
import { readSlackBlockActions, readSlackButton, SLACK_LINK_ACTION } from "./contracts/slack-callback.js";
|
|
16
15
|
import { object, slackId, slackTs, slackMember, SlackError, uploadSlackBytes, } from "./slack-api.js";
|
|
@@ -64,12 +63,12 @@ export function receiveSlack(state, identity, type, raw, now) {
|
|
|
64
63
|
const match = /^pair ([a-f0-9]{32})$/.exec(event.text.trim());
|
|
65
64
|
kind = match ? "pair" : "message";
|
|
66
65
|
payload = match
|
|
67
|
-
? { hash:
|
|
66
|
+
? { hash: chatHash(match[1]) }
|
|
68
67
|
: {
|
|
69
68
|
text: event.text.slice(0, MATE_MESSAGE_MAX_CHARS + 1),
|
|
70
69
|
originalLength: event.text.length,
|
|
71
70
|
};
|
|
72
|
-
id =
|
|
71
|
+
id = chatHash(`${identity.installation}:event:${body.event_id}`);
|
|
73
72
|
}
|
|
74
73
|
else if (type === "interactive" && body.type === "block_actions") {
|
|
75
74
|
const interaction = readSlackBlockActions(body);
|
|
@@ -88,7 +87,7 @@ export function receiveSlack(state, identity, type, raw, now) {
|
|
|
88
87
|
const button = readSlackButton(interaction.value);
|
|
89
88
|
payload = button.ok ? { token: button.value.value } : { problem: button.issues.map(issue => issue.line).join("; ") };
|
|
90
89
|
const tapped = actions[0];
|
|
91
|
-
id =
|
|
90
|
+
id = chatHash(`${identity.installation}:action:${member}:${ts}:${tapped?.value ?? ""}:${tapped?.action_ts ?? ""}`);
|
|
92
91
|
}
|
|
93
92
|
else
|
|
94
93
|
return false;
|
|
@@ -131,7 +130,7 @@ export function receiveSlack(state, identity, type, raw, now) {
|
|
|
131
130
|
}
|
|
132
131
|
const delivery = (options) => ({
|
|
133
132
|
...options,
|
|
134
|
-
state: new
|
|
133
|
+
state: new ChatState(options.store, "slack"),
|
|
135
134
|
label: "Slack",
|
|
136
135
|
member: (member, channel) => slackMember(options.api, options.identity, member, channel),
|
|
137
136
|
// A 👍 on the owner's message where the app has reactions:write; Slack shows bots no typing indicator.
|
|
@@ -201,26 +200,20 @@ function linkButton(origin, link) {
|
|
|
201
200
|
}
|
|
202
201
|
}
|
|
203
202
|
export async function deliverSlackPart(options) {
|
|
204
|
-
const state = new
|
|
203
|
+
const state = new ChatState(options.store, "slack"), { store, identity } = options, now = nowOf(options);
|
|
205
204
|
if (!state.owns(identity.installation, options.owner, now) ||
|
|
206
205
|
!options.current())
|
|
207
206
|
return false;
|
|
208
|
-
const row = state.
|
|
209
|
-
.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")
|
|
210
|
-
.get(identity.installation, options.canNotify?.() === false ? 0 : 1, now.toISOString());
|
|
207
|
+
const row = state.nextPart(identity.installation, options.canNotify?.() !== false, now);
|
|
211
208
|
if (!row)
|
|
212
209
|
return false;
|
|
213
210
|
const event = state.event(row.event), binding = event.binding === null ? null : state.bindingById(event.binding);
|
|
214
211
|
if (!binding) {
|
|
215
|
-
state.
|
|
216
|
-
.prepare("UPDATE slack_part SET state='dropped',problem='Chat access changed' WHERE id=?")
|
|
217
|
-
.run(row.id);
|
|
212
|
+
state.dropPart(row.id, "Chat access changed");
|
|
218
213
|
return true;
|
|
219
214
|
}
|
|
220
215
|
if (new Date(row.created).getTime() + 86_400_000 < now.getTime()) {
|
|
221
|
-
state.
|
|
222
|
-
.prepare("UPDATE slack_part SET state='dropped',problem='Delivery expired; open the saved chat to recover it' WHERE id=?")
|
|
223
|
-
.run(row.id);
|
|
216
|
+
state.dropPart(row.id, "Delivery expired; open the saved chat to recover it");
|
|
224
217
|
return true;
|
|
225
218
|
}
|
|
226
219
|
try {
|
|
@@ -235,15 +228,15 @@ export async function deliverSlackPart(options) {
|
|
|
235
228
|
!repos.includes(store.lookupRef(content.task)?.repo ?? ""))
|
|
236
229
|
throw new SlackError("Connected projects changed");
|
|
237
230
|
// A screenshot sent with a result goes in that result's thread once its message is placed, and waits until then.
|
|
238
|
-
const result = content.shot?.follows == null ?
|
|
239
|
-
if (result !==
|
|
240
|
-
state.
|
|
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));
|
|
241
234
|
return true;
|
|
242
235
|
}
|
|
243
|
-
const follows = result?.
|
|
236
|
+
const follows = result?.message;
|
|
244
237
|
const thread = slackTs(follows) ? String(follows) : event.thread;
|
|
245
238
|
if (content.image && content.shot && resultShotsPruned(options.evidenceRoot, content.image.run)) {
|
|
246
|
-
state.
|
|
239
|
+
state.dropPart(row.id, "Removed by retention");
|
|
247
240
|
return true;
|
|
248
241
|
}
|
|
249
242
|
if (content.image) {
|
|
@@ -262,17 +255,13 @@ export async function deliverSlackPart(options) {
|
|
|
262
255
|
typeof allocated.upload_url !== "string")
|
|
263
256
|
throw new SlackError("Slack did not identify the upload");
|
|
264
257
|
file = allocated.file_id;
|
|
265
|
-
state.
|
|
266
|
-
.prepare("UPDATE slack_part SET file=? WHERE id=?")
|
|
267
|
-
.run(file, row.id);
|
|
258
|
+
state.setPartFile(row.id, file);
|
|
268
259
|
await access(options, binding, session?.ceilingDigest);
|
|
269
260
|
const fresh = verifyResultImage(store, options.evidenceRoot, repos, content.image);
|
|
270
261
|
if (!fresh.ok)
|
|
271
262
|
throw new SlackError("Screenshot evidence changed before upload");
|
|
272
263
|
await (options.upload ?? uploadSlackBytes)(allocated.upload_url, fresh.bytes);
|
|
273
|
-
state.
|
|
274
|
-
.prepare("UPDATE slack_part SET uploaded=1 WHERE id=?")
|
|
275
|
-
.run(row.id);
|
|
264
|
+
state.setPartFile(row.id, null, true);
|
|
276
265
|
}
|
|
277
266
|
await access(options, binding, session?.ceilingDigest);
|
|
278
267
|
if (!verifyResultImage(store, options.evidenceRoot, repos, content.image)
|
|
@@ -288,9 +277,7 @@ export async function deliverSlackPart(options) {
|
|
|
288
277
|
.map(object)
|
|
289
278
|
.find((receipt) => (receipt.thread_ts ?? "") === thread && slackTs(receipt.ts));
|
|
290
279
|
if (found) {
|
|
291
|
-
state.
|
|
292
|
-
.prepare("UPDATE slack_part SET state='sent',message=?,problem=NULL WHERE id=?")
|
|
293
|
-
.run(String(found.ts), row.id);
|
|
280
|
+
state.partSent(row.id, String(found.ts), nowOf(options), false);
|
|
294
281
|
return true;
|
|
295
282
|
}
|
|
296
283
|
}
|
|
@@ -308,9 +295,7 @@ export async function deliverSlackPart(options) {
|
|
|
308
295
|
if (!Array.isArray(completed.files) ||
|
|
309
296
|
!completed.files.some((value) => object(value).id === file))
|
|
310
297
|
throw new SlackError("Slack did not confirm the file identity", 15_000, true);
|
|
311
|
-
state.
|
|
312
|
-
.prepare("UPDATE slack_part SET state='sent',problem=NULL WHERE id=?")
|
|
313
|
-
.run(row.id);
|
|
298
|
+
state.partSent(row.id, null, nowOf(options), false);
|
|
314
299
|
return true;
|
|
315
300
|
}
|
|
316
301
|
}
|
|
@@ -327,9 +312,7 @@ export async function deliverSlackPart(options) {
|
|
|
327
312
|
? armedCardText(proposal, preview.text)
|
|
328
313
|
: preview.text.replace("\n\nConfirm or Dismiss below. Nothing changes until you confirm.", "");
|
|
329
314
|
if (preview.buttons && preview.text.length <= 10_000) {
|
|
330
|
-
const tokens = state.
|
|
331
|
-
.prepare("SELECT token,phase FROM slack_action WHERE part=? AND consumed IS NULL AND expires>? ORDER BY rowid")
|
|
332
|
-
.all(row.id, now.toISOString());
|
|
315
|
+
const tokens = state.partTokens(row.id, now);
|
|
333
316
|
buttons = tokens.map((action) => ({
|
|
334
317
|
type: "button",
|
|
335
318
|
text: {
|
|
@@ -398,6 +381,10 @@ export async function deliverSlackPart(options) {
|
|
|
398
381
|
value: one.token,
|
|
399
382
|
}))
|
|
400
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}` }))),
|
|
401
388
|
...linkButton(options.origin(), content.link),
|
|
402
389
|
// [Look first] beside [Merge] or [Accept and finish]: each button's own action id.
|
|
403
390
|
...(content.also ?? []).flatMap((one, index) => linkButton(options.origin(), one).map(button => ({ ...button, action_id: `toolroll_link_${index + 2}` }))),
|
|
@@ -427,17 +414,13 @@ export async function deliverSlackPart(options) {
|
|
|
427
414
|
});
|
|
428
415
|
if (!slackTs(answer.ts) || (target && answer.ts !== target))
|
|
429
416
|
throw new SlackError("Slack did not confirm the message identity", 15_000, true);
|
|
430
|
-
state.
|
|
431
|
-
|
|
432
|
-
.run(answer.ts, row.id);
|
|
433
|
-
state.db
|
|
434
|
-
.prepare("UPDATE slack_runtime SET problem=NULL WHERE installation=? AND owner=?")
|
|
435
|
-
.run(identity.installation, options.owner);
|
|
417
|
+
state.partSent(row.id, String(answer.ts), nowOf(options));
|
|
418
|
+
state.setProblem(identity.installation, options.owner, null);
|
|
436
419
|
return true;
|
|
437
420
|
}
|
|
438
421
|
catch (error) {
|
|
439
422
|
if (error instanceof ChatDeliveryError && error.permanent) {
|
|
440
|
-
state.
|
|
423
|
+
state.dropPart(row.id, error.message);
|
|
441
424
|
return true;
|
|
442
425
|
}
|
|
443
426
|
// No permission to upload files here: one plain line instead of the result's screenshots.
|
|
@@ -449,18 +432,12 @@ export async function deliverSlackPart(options) {
|
|
|
449
432
|
const problem = error instanceof SlackError
|
|
450
433
|
? error
|
|
451
434
|
: new SlackError("Slack delivery is waiting to retry", 15_000, true);
|
|
452
|
-
state.
|
|
453
|
-
.prepare("UPDATE slack_runtime SET problem=? WHERE installation=? AND owner=?")
|
|
454
|
-
.run(problem.message, identity.installation, options.owner);
|
|
435
|
+
state.setProblem(identity.installation, options.owner, problem.message);
|
|
455
436
|
const until = new Date(nowOf(options).getTime() +
|
|
456
437
|
Math.max(problem.retryMs, [5000, 15000, 60000, 300000][Math.min(row.attempts, 3)])).toISOString();
|
|
457
|
-
state.
|
|
458
|
-
.prepare("UPDATE slack_part SET attempts=attempts+1,uncertain=uncertain+?,next_at=?,problem=? WHERE id=?")
|
|
459
|
-
.run(problem.uncertain ? 1 : 0, until, problem.message, row.id);
|
|
438
|
+
state.partFailed(row.id, { problem: problem.message, until, uncertain: problem.uncertain });
|
|
460
439
|
if (problem.code === "ratelimited")
|
|
461
|
-
state.
|
|
462
|
-
.prepare("UPDATE slack_runtime SET retry_at=? WHERE installation=?")
|
|
463
|
-
.run(until, identity.installation);
|
|
440
|
+
state.deferUntil(identity.installation, until);
|
|
464
441
|
return true;
|
|
465
442
|
}
|
|
466
443
|
}
|
package/dist/slack-settings.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadPrimary } from "./webhooks.js";
|
|
2
2
|
import { loadSlackCredentials } from "./slack-api.js";
|
|
3
|
-
import {
|
|
3
|
+
import { ChatState } from "./chat-delivery-state.js";
|
|
4
4
|
const escape = (text) => text
|
|
5
5
|
.replaceAll("&", "&")
|
|
6
6
|
.replaceAll("<", "<")
|
|
@@ -8,15 +8,11 @@ const escape = (text) => text
|
|
|
8
8
|
.replaceAll('"', """)
|
|
9
9
|
.replaceAll("'", "'");
|
|
10
10
|
export function slackSettingsHtml(store, dir, csrf, options = {}) {
|
|
11
|
-
const credentials = loadSlackCredentials(dir), state = new
|
|
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
|
-
|
|
19
|
-
runtime.
|
|
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 =
|
|
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 {
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
}
|