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.
- package/CHANGELOG.md +16 -0
- package/README.md +55 -80
- package/dist/agentconfig.d.ts +11 -36
- package/dist/agentconfig.js +3 -62
- package/dist/approval-policy.d.ts +1 -1
- package/dist/approval-policy.js +1 -1
- package/dist/board.d.ts +3 -19
- package/dist/board.js +4 -42
- package/dist/browser/workspace.js +18 -18
- package/dist/browser-workspace.d.ts +2 -1
- package/dist/builder.d.ts +9 -40
- package/dist/builder.js +50 -257
- package/dist/chat-actions.js +3 -14
- package/dist/chat-ask.js +8 -8
- package/dist/chat-controls.d.ts +0 -4
- package/dist/chat-controls.js +0 -1
- 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 +27 -24
- package/dist/chat-delivery-state.d.ts +51 -75
- package/dist/chat-delivery-state.js +140 -170
- package/dist/chat-delivery.d.ts +21 -4
- package/dist/chat-delivery.js +313 -195
- 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 +7 -2
- package/dist/chat-task-actions.d.ts +1 -1
- package/dist/chat-task-actions.js +0 -2
- package/dist/claim.d.ts +79 -244
- package/dist/claim.js +688 -1023
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +10 -2
- 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/cli.d.ts +0 -13
- package/dist/contracts/cli.js +0 -2
- package/dist/contracts/console-api.d.ts +5 -10
- package/dist/contracts/console-api.js +4 -10
- package/dist/contracts/discord-callback.js +2 -1
- package/dist/contracts/flow.d.ts +50 -0
- package/dist/contracts/flow.js +26 -1
- package/dist/contracts/lead-tools.d.ts +0 -13
- package/dist/contracts/lead-tools.js +3 -3
- package/dist/contracts/slack-callback.js +3 -2
- package/dist/contracts/store-json.d.ts +1 -29
- package/dist/contracts/store-json.js +4 -17
- package/dist/contracts/teams-callback.js +2 -1
- package/dist/converse.d.ts +1 -11
- package/dist/converse.js +6 -38
- package/dist/coordinator.d.ts +0 -1
- package/dist/coordinator.js +3 -5
- package/dist/demo.js +16 -26
- package/dist/desktop-update-gate.js +0 -1
- package/dist/desktop-update.js +1 -3
- package/dist/discord-chat.js +19 -38
- package/dist/discord-settings.js +4 -13
- package/dist/discord.js +4 -14
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +1 -1
- package/dist/dispose.d.ts +2 -3
- package/dist/dispose.js +17 -92
- package/dist/envelope.js +0 -3
- package/dist/exec.d.ts +0 -64
- package/dist/exec.js +3 -356
- package/dist/exhaustion.d.ts +22 -76
- package/dist/exhaustion.js +14 -140
- package/dist/export.js +8 -8
- package/dist/flow-engine.js +1 -1
- package/dist/flow-schedule.d.ts +154 -0
- package/dist/flow-schedule.js +321 -0
- package/dist/flow-share.js +1 -1
- package/dist/flow-triggers.d.ts +6 -1
- package/dist/flow-triggers.js +65 -10
- package/dist/flows-cli.js +8 -4
- package/dist/flows-ui.js +2 -0
- package/dist/flows.js +1 -1
- package/dist/guides.js +9 -29
- package/dist/invoke.d.ts +1 -24
- package/dist/invoke.js +17 -259
- package/dist/lead-status.js +2 -4
- package/dist/lead-voice.js +1 -1
- package/dist/mate-doors.d.ts +1 -2
- package/dist/mate-doors.js +12 -25
- package/dist/mate-tools.d.ts +2 -2
- package/dist/mate-tools.js +13 -24
- package/dist/modes.d.ts +19 -8
- package/dist/modes.js +24 -14
- package/dist/notification-rows.d.ts +30 -0
- package/dist/notification-rows.js +102 -0
- package/dist/operate-remote-arguments.js +2 -6
- package/dist/operate-remote.js +6 -7
- package/dist/operate.d.ts +0 -6
- package/dist/operate.js +226 -2248
- package/dist/phase-routing.d.ts +13 -23
- package/dist/phase-routing.js +13 -35
- package/dist/plan-auto.js +2 -2
- package/dist/plane-review.js +5 -11
- package/dist/planner-source.js +2 -1
- package/dist/policy.d.ts +1 -4
- package/dist/policy.js +0 -8
- package/dist/process-recovery-eligibility.js +2 -3
- package/dist/project-delete.js +24 -41
- package/dist/proposal.d.ts +4 -24
- package/dist/proposal.js +3 -88
- package/dist/provider-auth.d.ts +3 -3
- package/dist/provider-auth.js +4 -6
- package/dist/provider.d.ts +11 -25
- package/dist/provider.js +4 -36
- package/dist/push.js +1 -1
- package/dist/recipe-ui.js +6 -6
- package/dist/recipes.d.ts +3 -3
- package/dist/recipes.js +27 -23
- package/dist/result-shots.js +2 -2
- package/dist/retention.js +1 -1
- package/dist/run-closeout.d.ts +1 -1
- package/dist/run-closeout.js +1 -4
- package/dist/scope.d.ts +11 -65
- package/dist/scope.js +17 -82
- package/dist/serve.js +15 -24
- package/dist/server/chat.js +7 -30
- package/dist/server/flows.js +11 -10
- package/dist/server/guards.js +0 -1
- package/dist/server/pages-assets.js +1 -1
- package/dist/server/pages.js +0 -2
- package/dist/server/people-tokens.js +0 -4
- package/dist/server/route-table.d.ts +1 -1
- package/dist/server/route-table.js +2 -17
- package/dist/server/settings.js +6 -12
- package/dist/server/shared.d.ts +28 -212
- package/dist/server/shared.js +71 -780
- package/dist/server/task-views.js +9 -76
- package/dist/server/tasks.d.ts +1 -50
- package/dist/server/tasks.js +36 -946
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-api.d.ts +5 -1
- package/dist/slack-api.js +2 -2
- package/dist/slack-chat.d.ts +9 -6
- package/dist/slack-chat.js +29 -53
- package/dist/slack-settings.js +6 -15
- package/dist/slack.js +6 -16
- package/dist/store.d.ts +200 -1702
- package/dist/store.js +811 -5439
- package/dist/surface.js +10 -24
- package/dist/task-composer.d.ts +1 -1
- package/dist/task-composer.js +1 -1
- package/dist/task-control.d.ts +3 -10
- package/dist/task-control.js +2 -7
- package/dist/teammate-desk.js +1 -1
- package/dist/teammate-question.js +12 -12
- package/dist/teammates-ui.js +1 -1
- 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-mate.d.ts +0 -3
- package/dist/telegram-mate.js +0 -1
- package/dist/telegram.js +4 -4
- package/dist/templates.d.ts +11 -7
- package/dist/templates.js +12 -9
- package/dist/toolroll-update.d.ts +20 -0
- package/dist/toolroll-update.js +104 -5
- package/dist/work-index.js +2 -6
- package/dist/workspace-revision.js +3 -1
- package/dist/workspace-ui.js +1 -1
- package/package.json +1 -1
- package/scripts/recovery-canary.mjs +3 -3
- package/dist/contest.d.ts +0 -318
- package/dist/contest.js +0 -753
- package/dist/held.d.ts +0 -150
- package/dist/held.js +0 -799
- package/dist/liveness.d.ts +0 -30
- package/dist/liveness.js +0 -42
- package/dist/pricing.d.ts +0 -45
- package/dist/pricing.js +0 -77
- package/dist/routine.d.ts +0 -228
- package/dist/routine.js +0 -764
- package/dist/slack-state.d.ts +0 -13
- package/dist/slack-state.js +0 -10
- package/dist/supervisor.mjs +0 -297
package/dist/chat-delivery.js
CHANGED
|
@@ -14,7 +14,7 @@ import { answerChatFlowPrompt, answerChoiceMessage, applyChatFlowTap, flowDecisi
|
|
|
14
14
|
import { connectChannel, FLOW_WORDS, takeChannelMessage, watchedChannel } from "./chat-inbox.js";
|
|
15
15
|
import { triggerConfigOf } from "./flow-triggers.js";
|
|
16
16
|
import { telegramProgressCard } from "./telegram-progress.js";
|
|
17
|
-
import { enqueueEveningDigests, finishedView, isTaskFact, joinsBatch, needsPerson, quietCardView } from "./chat-quiet.js";
|
|
17
|
+
import { enqueueEveningDigests, finishedView, isTaskFact, joinsBatch, needsPerson, quietCardView, wantedBeforePairing } from "./chat-quiet.js";
|
|
18
18
|
import { BATCH_MS, chatText, chatTitle } from "./chat-voice.js";
|
|
19
19
|
import { LEAD_SAY_KIND, enqueueLeadLapses, leadSayEarlier, leadSayText, leadSubjectOf } from "./lead-voice.js";
|
|
20
20
|
import { leadChannelOf } from "./lead-context.js";
|
|
@@ -25,6 +25,8 @@ import { isTelegramProgressNotification, proposalTaskOf } from "./store.js";
|
|
|
25
25
|
import { isShotsKind, resultShotsFor } from "./result-shots.js";
|
|
26
26
|
import { messageTeammate } from "./teammate-desk.js";
|
|
27
27
|
import { CHAT_APP_NAMES } from "./flow-triggers.js";
|
|
28
|
+
import { answersPrompt, applyDecideFeedback, applyDecideTap, decideFallbackLink, decideOffer, decideTargetOf, hasLiveDecideTokens, linksFor, mergedText, mintDecideButtons, offerFingerprint, openPromptFor, recordChatMerge } from "./chat-decide.js";
|
|
29
|
+
import { mergePullRequest } from "./pull-request-flow.js";
|
|
28
30
|
export const chatObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v)
|
|
29
31
|
? v
|
|
30
32
|
: {};
|
|
@@ -69,6 +71,72 @@ export async function channelAccess(options, binding, ceiling) {
|
|
|
69
71
|
throw new ChatDeliveryError("Chat access changed");
|
|
70
72
|
return latest;
|
|
71
73
|
}
|
|
74
|
+
// ---- decisions that finish in the chat (chat-decide.ts), the same in every app --------------------------------------
|
|
75
|
+
/** Where a paired person taps: their binding, and the chat the card is in. */
|
|
76
|
+
const decideSeat = (state, binding, chat = binding.channel) => ({ channel: state.provider, binding: binding.id, chat, approver: binding.approver, generation: binding.generation });
|
|
77
|
+
/** What a card about one result, plan, failure or pull request may act on in this person's chat now, under their
|
|
78
|
+
* current ceiling; null keeps its link. Reads only. */
|
|
79
|
+
function decideOfferFor(options, binding, target, repos, now) {
|
|
80
|
+
if (target == null)
|
|
81
|
+
return null;
|
|
82
|
+
const principal = verifyApproverStanding(options.store, binding.approver, binding.generation, repos);
|
|
83
|
+
return principal.ok ? decideOffer(options.store, target, principal.who, now, options.state.provider, options.evidenceRoot) : null;
|
|
84
|
+
}
|
|
85
|
+
/** A card's buttons as a part carries them. */
|
|
86
|
+
const decideRows = (rows) => rows.map(row => row.map(one => "token" in one ? { label: one.label, token: one.token } : { label: one.label, link: one.link }));
|
|
87
|
+
/** The offer's buttons, minted now: they ride the part's message once it is sent (ChatState.partSent). */
|
|
88
|
+
function mintDecide(options, binding, target, offer, now) {
|
|
89
|
+
return { rows: decideRows(mintDecideButtons(options.store, decideSeat(options.state, binding), target, offer, now).rows) };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A decide button (chat-decide.ts), inside the tap's transaction: the same door, words and two taps as on Telegram.
|
|
93
|
+
* The card it rode is repainted in place; Request changes asks for the next message; a merge goes out after.
|
|
94
|
+
*/
|
|
95
|
+
function applyChatDecideTap(options, event, binding, token, repos, now) {
|
|
96
|
+
const { store, state } = options;
|
|
97
|
+
// The card the button rode: this person's part on that message.
|
|
98
|
+
const row = state.prepare(`SELECT p.id, p.payload FROM chat_part p JOIN chat_event e ON e.provider = p.provider AND e.id = p.event
|
|
99
|
+
WHERE p.provider = :provider AND e.binding = ? AND p.message = ? ORDER BY p.id DESC LIMIT 1`).get(binding.id, event.ts);
|
|
100
|
+
const read = row === undefined ? null : readChatPart(String(row["payload"]));
|
|
101
|
+
const card = read?.ok === true ? read.value : null;
|
|
102
|
+
const tapped = applyDecideTap(store, decideSeat(state, binding, event.channel), { token, message: event.ts, shown: card?.text ?? "", repos, root: options.evidenceRoot, now });
|
|
103
|
+
if (tapped === null) {
|
|
104
|
+
state.plan(event.id, [{ text: "That button doesn't do anything now." }], now);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const replies = [];
|
|
108
|
+
let repainted = null;
|
|
109
|
+
if (tapped.edit !== undefined && row !== undefined && card !== null) {
|
|
110
|
+
const { decide: _decide, link: _link, also: _also, edit: _edit, ...kept } = card;
|
|
111
|
+
repainted = { ...kept, text: tapped.edit.text, ...(tapped.edit.rows.length === 0 ? {} : { decide: { rows: decideRows(tapped.edit.rows) } }) };
|
|
112
|
+
state.repaint(Number(row["id"]), repainted);
|
|
113
|
+
}
|
|
114
|
+
else
|
|
115
|
+
replies.push({ text: tapped.ack });
|
|
116
|
+
if (tapped.prompt !== undefined)
|
|
117
|
+
replies.push({ text: tapped.prompt.text, prompt: tapped.prompt.id });
|
|
118
|
+
state.plan(event.id, replies, now);
|
|
119
|
+
return tapped.merge === undefined || row === undefined || repainted === null ? null
|
|
120
|
+
: { merge: tapped.merge, part: Number(row["id"]), shown: tapped.edit?.text ?? card?.text ?? "", content: repainted };
|
|
121
|
+
}
|
|
122
|
+
/** GitHub is a network call: after the tap's transaction, then the card says how it went, and the ledger too. */
|
|
123
|
+
async function mergeFromChat(options, pending) {
|
|
124
|
+
const { merge } = pending;
|
|
125
|
+
let merged;
|
|
126
|
+
try {
|
|
127
|
+
const result = options.merge !== undefined ? await options.merge({ runId: merge.runId, by: merge.by })
|
|
128
|
+
: await mergePullRequest(options.store, { runId: merge.runId, by: merge.by, clock: () => nowOf(options) });
|
|
129
|
+
merged = result.ok ? { ok: true } : { ok: false, message: result.message };
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
merged = { ok: false, message: "GitHub couldn't be reached just now. Try again from the task." };
|
|
133
|
+
}
|
|
134
|
+
recordChatMerge(options.store, merge, merged, nowOf(options));
|
|
135
|
+
const { decide: _decide, ...kept } = pending.content;
|
|
136
|
+
// A merge that didn't land keeps a way to the task.
|
|
137
|
+
options.state.repaint(pending.part, { ...kept, text: mergedText(pending.shown, merged),
|
|
138
|
+
...(merged.ok ? {} : { decide: { rows: decideRows(linksFor({ kind: "merge", taskId: merge.taskId, run: merge.runId })) } }) });
|
|
139
|
+
}
|
|
72
140
|
export async function processChatEvent(options) {
|
|
73
141
|
const { store, identity } = options, state = options.state, now = nowOf(options);
|
|
74
142
|
if (!state.owns(identity.installation, options.owner, now) ||
|
|
@@ -88,14 +156,14 @@ export async function processChatEvent(options) {
|
|
|
88
156
|
return false;
|
|
89
157
|
const body = readChatPairBody(event.payload);
|
|
90
158
|
const paired = body.ok
|
|
91
|
-
? state.pair(identity, body.value.hash, event.member, event.channel, nowOf(options))
|
|
159
|
+
? state.pair(identity, body.value.hash, event.member, event.channel, nowOf(options), event.id)
|
|
92
160
|
: null;
|
|
93
161
|
if (!paired) {
|
|
94
162
|
state.finish(event.id, true);
|
|
95
163
|
return true;
|
|
96
164
|
}
|
|
97
165
|
state
|
|
98
|
-
.prepare("UPDATE chat_event SET binding=? WHERE id=?")
|
|
166
|
+
.prepare("UPDATE chat_event SET binding=? WHERE provider=:provider AND id=?")
|
|
99
167
|
.run(paired.id, event.id);
|
|
100
168
|
state.plan(event.id, [
|
|
101
169
|
{
|
|
@@ -114,7 +182,9 @@ export async function processChatEvent(options) {
|
|
|
114
182
|
}
|
|
115
183
|
const repos = await channelAccess(options, binding);
|
|
116
184
|
if (event.kind === "action") {
|
|
117
|
-
applyChatAction(options, event, binding, repos);
|
|
185
|
+
const merging = applyChatAction(options, event, binding, repos);
|
|
186
|
+
if (merging !== null)
|
|
187
|
+
await mergeFromChat(options, merging);
|
|
118
188
|
return true;
|
|
119
189
|
}
|
|
120
190
|
const body = readChatMessageBody(event.payload);
|
|
@@ -134,12 +204,22 @@ export async function processChatEvent(options) {
|
|
|
134
204
|
// A flow decision asked for this person's next message (Edit, Send back): it is the draft or the note.
|
|
135
205
|
if (answerChatFlowPrompt({ store, state, label: options.label }, event, binding, { text, ...(typeof input.originalLength === "number" ? { originalLength: input.originalLength } : {}) }, repos, nowOf(options)))
|
|
136
206
|
return true;
|
|
207
|
+
// After Request changes asked "What should change?": the person's next message in their own chat is the feedback
|
|
208
|
+
// (a reply in another message's thread keeps its own meaning).
|
|
209
|
+
if (event.channel === binding.channel && text.trim() !== "" && phoneCommand(text) === null) {
|
|
210
|
+
const prompt = openPromptFor(store, { channel: state.provider, binding: binding.id, chat: binding.channel }, nowOf(options));
|
|
211
|
+
if (prompt !== null && answersPrompt(prompt, event.thread !== "" && event.thread !== event.ts ? event.thread : null)) {
|
|
212
|
+
const answer = applyDecideFeedback(store, decideSeat(state, binding), prompt, text, repos, options.evidenceRoot, nowOf(options));
|
|
213
|
+
state.plan(event.id, [{ text: answer.said, ...(answer.link === null ? {} : { link: answer.link }) }], nowOf(options));
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
137
217
|
// A "Person chooses" notice's thread reply is the note; the first other message after it is asked about (chat-flow.ts).
|
|
138
218
|
if (answerChoiceMessage({ store, state, label: options.label }, event, binding, { text, ...(typeof input.originalLength === "number" ? { originalLength: input.originalLength } : {}), lead: input.lead === true }, nowOf(options), repos))
|
|
139
219
|
return true;
|
|
140
220
|
// A message to a teammate by name (v96), in someone's own chat with Toolroll: a card on its desk.
|
|
141
221
|
if (event.channel === binding.channel) {
|
|
142
|
-
const handed = messageTeammate(store, { who: binding.approver, repos, via: CHAT_APP_NAMES[state.
|
|
222
|
+
const handed = messageTeammate(store, { who: binding.approver, repos, via: CHAT_APP_NAMES[state.provider] ?? "Chat" }, text, nowOf(options));
|
|
143
223
|
if (handed !== null) {
|
|
144
224
|
state.plan(event.id, [{ text: handed.said, ...(handed.link === undefined ? {} : { link: handed.link }) }], nowOf(options));
|
|
145
225
|
return true;
|
|
@@ -190,7 +270,7 @@ export async function processChatEvent(options) {
|
|
|
190
270
|
const replies = [];
|
|
191
271
|
const registry = await options.readProjects();
|
|
192
272
|
const consumed = applyRoomInbound({
|
|
193
|
-
store, channel: state.
|
|
273
|
+
store, channel: state.provider, backend: state.roomBackend(identity.installation, now), chatId: event.channel, isGroup: isRoom,
|
|
194
274
|
sender: { approver: binding.approver, generation: binding.generation, binding: binding.id }, updateKey: event.id, text,
|
|
195
275
|
projects: registry, origin: options.origin(), now, report: { ignored: 0 },
|
|
196
276
|
say: (reply, link) => replies.push({ text: reply, ...(link ? { link } : {}), ...(isRoom ? { channel: event.channel } : {}) }),
|
|
@@ -208,7 +288,7 @@ export async function processChatEvent(options) {
|
|
|
208
288
|
], nowOf(options));
|
|
209
289
|
return true;
|
|
210
290
|
}
|
|
211
|
-
const request = chatHash(`${options.state.
|
|
291
|
+
const request = chatHash(`${options.state.provider}:${binding.id}:${event.id}`).slice(0, 32);
|
|
212
292
|
let receipt = event.session === null
|
|
213
293
|
? null
|
|
214
294
|
: store.mateRequestReceipt(event.session, request);
|
|
@@ -241,10 +321,10 @@ export async function processChatEvent(options) {
|
|
|
241
321
|
return true;
|
|
242
322
|
}
|
|
243
323
|
state
|
|
244
|
-
.prepare("UPDATE chat_event SET session=? WHERE id=? AND state='queued'")
|
|
324
|
+
.prepare("UPDATE chat_event SET session=? WHERE provider=:provider AND id=? AND state='queued'")
|
|
245
325
|
.run(resolved.session.id, event.id);
|
|
246
326
|
const contexts = state
|
|
247
|
-
.prepare("SELECT p.payload FROM chat_part p JOIN chat_event e ON e.id=p.event WHERE e.binding=? AND e.channel=? AND (p.message=? OR e.thread=?) AND p.state='sent' ORDER BY p.id DESC LIMIT 100")
|
|
327
|
+
.prepare("SELECT p.payload FROM chat_part p JOIN chat_event e ON e.provider=p.provider AND e.id=p.event WHERE p.provider=:provider AND e.binding=? AND e.channel=? AND (p.message=? OR e.thread=?) AND p.state='sent' ORDER BY p.id DESC LIMIT 100")
|
|
248
328
|
.all(binding.id, binding.channel, event.thread, event.thread)
|
|
249
329
|
// Context only: a part that can't be read adds none.
|
|
250
330
|
.flatMap((row) => { const read = readChatPart(String(row.payload)); return read.ok ? [read.value] : []; })
|
|
@@ -275,7 +355,7 @@ export async function processChatEvent(options) {
|
|
|
275
355
|
if (context?.task) {
|
|
276
356
|
about = { task: context.task, run: context.run ?? null };
|
|
277
357
|
state
|
|
278
|
-
.prepare("UPDATE chat_event SET payload=? WHERE id=? AND state='queued'")
|
|
358
|
+
.prepare("UPDATE chat_event SET payload=? WHERE provider=:provider AND id=? AND state='queued'")
|
|
279
359
|
.run(JSON.stringify({ ...input, about }), event.id);
|
|
280
360
|
}
|
|
281
361
|
// An unknown surface names no channel rather than a guess.
|
|
@@ -397,16 +477,16 @@ export async function processChatEvent(options) {
|
|
|
397
477
|
: new ChatDeliveryError(`${options.label} is waiting to retry`);
|
|
398
478
|
if (problem.permanent) {
|
|
399
479
|
state.plan(event.id, [{ text: problem.message }], nowOf(options));
|
|
400
|
-
state.prepare("UPDATE chat_event SET next_at=NULL,problem=? WHERE id=?").run(problem.message, event.id);
|
|
480
|
+
state.prepare("UPDATE chat_event SET next_at=NULL,problem=? WHERE provider=:provider AND id=?").run(problem.message, event.id);
|
|
401
481
|
return true;
|
|
402
482
|
}
|
|
403
483
|
state
|
|
404
|
-
.prepare("UPDATE chat_runtime SET problem=? WHERE installation=? AND owner=?")
|
|
484
|
+
.prepare("UPDATE chat_runtime SET problem=? WHERE provider=:provider AND installation=? AND owner=?")
|
|
405
485
|
.run(problem.message, identity.installation, options.owner);
|
|
406
486
|
state.defer(event.id, problem.message, new Date(nowOf(options).getTime() + problem.retryMs));
|
|
407
487
|
if (problem.code === "ratelimited")
|
|
408
488
|
state
|
|
409
|
-
.prepare("UPDATE chat_runtime SET retry_at=? WHERE installation=?")
|
|
489
|
+
.prepare("UPDATE chat_runtime SET retry_at=? WHERE provider=:provider AND installation=?")
|
|
410
490
|
.run(new Date(nowOf(options).getTime() + problem.retryMs).toISOString(), identity.installation);
|
|
411
491
|
return true;
|
|
412
492
|
}
|
|
@@ -416,7 +496,7 @@ export async function processChatEvent(options) {
|
|
|
416
496
|
* or that the channel's flow takes as a card. False: not for an inbox.
|
|
417
497
|
*/
|
|
418
498
|
async function channelInboxEvent(options, event) {
|
|
419
|
-
const { store, identity } = options, state = options.state, app = state.
|
|
499
|
+
const { store, identity } = options, state = options.state, app = state.provider;
|
|
420
500
|
const binding = event.binding === null ? null : state.bindingById(event.binding);
|
|
421
501
|
if (binding !== null && event.channel === binding.channel)
|
|
422
502
|
return false;
|
|
@@ -454,13 +534,14 @@ async function channelInboxEvent(options, event) {
|
|
|
454
534
|
state.finish(event.id);
|
|
455
535
|
return true;
|
|
456
536
|
}
|
|
457
|
-
state.prepare("UPDATE chat_event SET binding=? WHERE id=?").run(grant.id, event.id);
|
|
537
|
+
state.prepare("UPDATE chat_event SET binding=? WHERE provider=:provider AND id=?").run(grant.id, event.id);
|
|
458
538
|
state.plan(event.id, [{ text: taken.said, channel: event.channel, ...(taken.link === undefined ? {} : { link: taken.link }) }], nowOf(options));
|
|
459
539
|
return true;
|
|
460
540
|
}
|
|
461
541
|
/** Bound to the paired member, exact channel/message, pending proposal and current ceiling. */
|
|
462
542
|
export function applyChatAction(options, event, binding, repos) {
|
|
463
543
|
const state = options.state, { store } = options, now = nowOf(options), signals = [];
|
|
544
|
+
let merging = null;
|
|
464
545
|
store.transact(() => {
|
|
465
546
|
if (!state.owns(options.identity.installation, options.owner, now) ||
|
|
466
547
|
!options.current() ||
|
|
@@ -476,6 +557,11 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
476
557
|
return;
|
|
477
558
|
}
|
|
478
559
|
const token = body.value.token;
|
|
560
|
+
// A result's, plan's, failure's or pull request's own button (chat-decide.ts).
|
|
561
|
+
if (token.startsWith("d:")) {
|
|
562
|
+
merging = applyChatDecideTap(options, event, binding, token, repos, now);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
479
565
|
// A flow decision's button (v88) is answered by the flow's own door.
|
|
480
566
|
if (applyChatFlowTap({ store, state, label: options.label }, event, binding, token, repos, now))
|
|
481
567
|
return;
|
|
@@ -486,7 +572,7 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
486
572
|
if (applyChatAskTap({ store, state }, event, binding, token, now))
|
|
487
573
|
return;
|
|
488
574
|
const saved = state
|
|
489
|
-
.prepare("SELECT a
|
|
575
|
+
.prepare("SELECT a.token,a.part,a.proposal,a.phase,a.expires,a.consumed,p.message,e.binding,e.channel,e.thread FROM chat_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=?")
|
|
490
576
|
.get(token);
|
|
491
577
|
// The saved button, read by its schema: one that can't be read is a spent button, never a guess.
|
|
492
578
|
const read = saved === undefined ? null : readProposalActionRow({ ...saved });
|
|
@@ -495,7 +581,7 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
495
581
|
action.binding !== binding.id ||
|
|
496
582
|
action.channel !== event.channel ||
|
|
497
583
|
action.message !== event.ts ||
|
|
498
|
-
(options.state.
|
|
584
|
+
(options.state.provider === "slack" && action.thread !== event.thread) ||
|
|
499
585
|
action.consumed !== null ||
|
|
500
586
|
action.expires <= now.toISOString();
|
|
501
587
|
if (invalid) {
|
|
@@ -517,7 +603,7 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
517
603
|
proposal: proposal.id,
|
|
518
604
|
edit: event.ts,
|
|
519
605
|
};
|
|
520
|
-
const preview = proposalPreview(store, proposal, repos, options.state.
|
|
606
|
+
const preview = proposalPreview(store, proposal, repos, options.state.provider);
|
|
521
607
|
const phase = action.phase;
|
|
522
608
|
if (proposal.state !== "pending")
|
|
523
609
|
content = { text: proposalOutcomeText(proposal), edit: event.ts };
|
|
@@ -544,10 +630,9 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
544
630
|
}
|
|
545
631
|
else {
|
|
546
632
|
const outcome = confirmMateProposal(store, verified.who, proposal.id, now, {
|
|
547
|
-
via: options.state.
|
|
633
|
+
via: options.state.provider,
|
|
548
634
|
evidenceRoot: options.evidenceRoot,
|
|
549
635
|
confirm: phase === "yes",
|
|
550
|
-
...(options.held ? { held: options.held } : {}),
|
|
551
636
|
deferSignal: (signal) => signals.push(signal),
|
|
552
637
|
});
|
|
553
638
|
if (!outcome.ok && outcome.reason === "needs-confirm") {
|
|
@@ -567,20 +652,21 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
567
652
|
}
|
|
568
653
|
// Repaint the original persisted card; retain one placement and fresh tokens.
|
|
569
654
|
state
|
|
570
|
-
.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?")
|
|
655
|
+
.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE provider=:provider AND id=?")
|
|
571
656
|
.run(savedChatPart(content), action.part);
|
|
572
657
|
if (!content.proposal)
|
|
573
658
|
state
|
|
574
|
-
.prepare("UPDATE chat_action SET consumed=? WHERE proposal=? AND consumed IS NULL")
|
|
659
|
+
.prepare("UPDATE chat_action SET consumed=? WHERE provider=:provider AND proposal=? AND consumed IS NULL")
|
|
575
660
|
.run(now.toISOString(), proposal.id);
|
|
576
661
|
else
|
|
577
662
|
state
|
|
578
|
-
.prepare("UPDATE chat_action SET consumed=? WHERE token=?")
|
|
663
|
+
.prepare("UPDATE chat_action SET consumed=? WHERE provider=:provider AND token=?")
|
|
579
664
|
.run(now.toISOString(), token);
|
|
580
665
|
state.finish(event.id);
|
|
581
666
|
});
|
|
582
667
|
for (const signal of signals)
|
|
583
668
|
signal();
|
|
669
|
+
return merging;
|
|
584
670
|
}
|
|
585
671
|
/**
|
|
586
672
|
* Carry new conversation messages to every room that follows one: the
|
|
@@ -610,7 +696,7 @@ export async function planRoomMessages(options) {
|
|
|
610
696
|
if (row === null)
|
|
611
697
|
continue;
|
|
612
698
|
for (const message of roomMessagesAfter(store, row.thread, room.cursor)) {
|
|
613
|
-
const text = roomMessageText(message, state.
|
|
699
|
+
const text = roomMessageText(message, state.provider, room.chat);
|
|
614
700
|
let carrier = null;
|
|
615
701
|
if (room.kind === "private")
|
|
616
702
|
carrier = live.find(one => one.channel === room.chat) ?? null;
|
|
@@ -632,7 +718,7 @@ export async function planRoomMessages(options) {
|
|
|
632
718
|
if (message.role === "assistant" && message.turn !== null)
|
|
633
719
|
for (const proposal of store.listMateProposals(row.thread, ["pending"]).filter(one => one.turn === message.turn))
|
|
634
720
|
parts.push({ text: "", proposal: proposal.id, channel: room.chat });
|
|
635
|
-
const id = chatHash(`${state.
|
|
721
|
+
const id = chatHash(`${state.provider}:room:${room.id}:${message.id}`);
|
|
636
722
|
const target = carrier;
|
|
637
723
|
store.transact(() => {
|
|
638
724
|
if (parts.length > 0 && state.enqueue({ id, installation: identity.installation, binding: target.id, kind: "message", channel: room.chat, member: target.member, ts: "", thread: "", payload: {}, created: now.toISOString() }))
|
|
@@ -648,7 +734,7 @@ function planQuietCard(options, binding, notification, now) {
|
|
|
648
734
|
const { state, store, identity } = options;
|
|
649
735
|
if (notification.taskRef === null || notification.taskId === null)
|
|
650
736
|
return;
|
|
651
|
-
const card = store.chatCardFor(`${state.
|
|
737
|
+
const card = store.chatCardFor(`${state.provider}:${binding.id}`, notification.taskRef, store.getTask(notification.taskId)?.createdAt ?? notification.createdAt, now);
|
|
652
738
|
const view = quietCardView(store, card.tasks, now, options.evidenceRoot, binding.approver);
|
|
653
739
|
if (view === null)
|
|
654
740
|
return;
|
|
@@ -657,45 +743,53 @@ function planQuietCard(options, binding, notification, now) {
|
|
|
657
743
|
if (card.message !== null) {
|
|
658
744
|
if (card.digest === shown)
|
|
659
745
|
return;
|
|
660
|
-
state.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE id=? AND state!='dropped'").run(savedChatPart(content), now.toISOString(), Number(card.message));
|
|
746
|
+
state.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE provider=:provider AND id=? AND state!='dropped'").run(savedChatPart(content), now.toISOString(), Number(card.message));
|
|
661
747
|
store.setChatCardMessage(card.id, card.message, shown);
|
|
662
748
|
return;
|
|
663
749
|
}
|
|
664
|
-
const id = chatHash(`${state.
|
|
750
|
+
const id = chatHash(`${state.provider}:card:${binding.id}:${card.id}`);
|
|
665
751
|
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
666
752
|
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
667
753
|
state.plan(id, [content], now);
|
|
668
|
-
const part = state.prepare("SELECT id FROM chat_part WHERE event=?").get(id);
|
|
754
|
+
const part = state.prepare("SELECT id FROM chat_part WHERE provider=:provider AND event=?").get(id);
|
|
669
755
|
if (part !== undefined)
|
|
670
756
|
store.setChatCardMessage(card.id, String(part.id), shown);
|
|
671
757
|
}
|
|
672
758
|
/** Any update for this person joins their open batch (two minutes from its first), and the batch's one message
|
|
673
759
|
* is planned once, then repainted in place as more updates land. `notification`: an update that is not a finished
|
|
674
760
|
* result (null for one that is). False when nothing in the batch reads as a line, so the update goes out on its own. */
|
|
675
|
-
function planFinished(options, binding, taskRef, run, now, notification = null) {
|
|
761
|
+
function planFinished(options, binding, taskRef, run, now, notification = null, repos = []) {
|
|
676
762
|
const { state, store, identity } = options;
|
|
677
|
-
const batch = store.chatBatchFor(`${state.
|
|
763
|
+
const batch = store.chatBatchFor(`${state.provider}:${binding.id}`, taskRef, run, now, BATCH_MS, notification);
|
|
678
764
|
const view = finishedView(store, batch, now, options.evidenceRoot, binding.approver);
|
|
679
765
|
if (view === null)
|
|
680
766
|
return false;
|
|
681
767
|
// A single update keeps its task and run, so a reply to it names that work.
|
|
682
768
|
const single = batch.items.length === 1 ? batch.items[0] : null;
|
|
683
769
|
const task = single === null ? null : store.refById(single.taskRef)?.externalId ?? null;
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
770
|
+
const about = { ...(task === null ? {} : { task }), ...(single?.run == null ? {} : { run: single.run }) };
|
|
771
|
+
// A message about one result, plan, failure or pull request acts in place (chat-decide.ts), as on Telegram: its
|
|
772
|
+
// buttons are minted only when what it offers changed, or its own were spent (Not now), so an unchanged repaint keeps them.
|
|
773
|
+
const offer = decideOfferFor(options, binding, view.target, repos, now);
|
|
774
|
+
const content = offer === null
|
|
775
|
+
? { text: view.text, link: view.link, ...(view.also === undefined ? {} : { also: view.also }), ...about }
|
|
776
|
+
: { text: offer.text ?? view.text, ...about };
|
|
777
|
+
const shown = chatHash(JSON.stringify(offer === null ? content : [content, offerFingerprint(offer)]));
|
|
687
778
|
if (batch.message !== null) {
|
|
688
|
-
|
|
779
|
+
const placed = state.part(Number(batch.message));
|
|
780
|
+
const live = offer === null || placed?.state === "pending" || (placed?.message != null && hasLiveDecideTokens(store, state.provider, binding.channel, placed.message, now));
|
|
781
|
+
if (batch.digest === shown && live)
|
|
689
782
|
return true;
|
|
690
|
-
|
|
783
|
+
const painted = offer === null ? content : { ...content, decide: mintDecide(options, binding, view.target, offer, now) };
|
|
784
|
+
state.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE provider=:provider AND id=? AND state!='dropped'").run(savedChatPart(painted), now.toISOString(), Number(batch.message));
|
|
691
785
|
store.setChatBatchMessage(batch.id, batch.message, shown);
|
|
692
786
|
return true;
|
|
693
787
|
}
|
|
694
|
-
const id = chatHash(`${state.
|
|
788
|
+
const id = chatHash(`${state.provider}:batch:${binding.id}:${batch.id}`);
|
|
695
789
|
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
696
790
|
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
697
|
-
state.plan(id, [content], now);
|
|
698
|
-
const part = state.prepare("SELECT id FROM chat_part WHERE event=?").get(id);
|
|
791
|
+
state.plan(id, [offer === null ? content : { ...content, decide: mintDecide(options, binding, view.target, offer, now) }], now);
|
|
792
|
+
const part = state.prepare("SELECT id FROM chat_part WHERE provider=:provider AND event=?").get(id);
|
|
699
793
|
if (part !== undefined)
|
|
700
794
|
store.setChatBatchMessage(batch.id, String(part.id), shown);
|
|
701
795
|
return true;
|
|
@@ -716,20 +810,20 @@ function planResultShots(options, binding, notification, repos, id, now) {
|
|
|
716
810
|
if (visit !== null) {
|
|
717
811
|
for (const key of [`flow-send:${visit[1]}:${visit[2]}`, `flow-choose:${visit[1]}:${visit[2]}`]) {
|
|
718
812
|
const notice = store.handle.prepare("SELECT id FROM notification WHERE dedupe_key=?").get(key);
|
|
719
|
-
const part = notice === undefined ? undefined : state.prepare("SELECT id FROM chat_part WHERE event=? ORDER BY id DESC LIMIT 1").get(chatHash(`${state.
|
|
813
|
+
const part = notice === undefined ? undefined : state.prepare("SELECT id FROM chat_part WHERE provider=:provider AND event=? ORDER BY id DESC LIMIT 1").get(chatHash(`${state.provider}:notice:${binding.id}:${Number(notice["id"])}`));
|
|
720
814
|
if (part !== undefined)
|
|
721
815
|
return Number(part.id);
|
|
722
816
|
}
|
|
723
817
|
return null;
|
|
724
818
|
}
|
|
725
|
-
const batch = notification.taskRef === null ? null : store.chatBatchMessageFor(`${state.
|
|
819
|
+
const batch = notification.taskRef === null ? null : store.chatBatchMessageFor(`${state.provider}:${binding.id}`, notification.taskRef, notification.run);
|
|
726
820
|
if (batch !== null)
|
|
727
821
|
return Number(batch);
|
|
728
|
-
const card = state.prepare("SELECT part FROM chat_progress WHERE binding=? AND run=?").get(binding.id, notification.run);
|
|
822
|
+
const card = state.prepare("SELECT part FROM chat_progress WHERE provider=:provider AND binding=? AND run=?").get(binding.id, notification.run);
|
|
729
823
|
if (card !== undefined)
|
|
730
824
|
return Number(card.part);
|
|
731
825
|
for (const one of store.handle.prepare("SELECT id FROM notification WHERE source_run=? AND recipient IS NULL AND id<? ORDER BY id DESC").all(notification.run, notification.id)) {
|
|
732
|
-
const part = state.prepare("SELECT id FROM chat_part WHERE event=? ORDER BY id LIMIT 1").get(chatHash(`${state.
|
|
826
|
+
const part = state.prepare("SELECT id FROM chat_part WHERE provider=:provider AND event=? ORDER BY id LIMIT 1").get(chatHash(`${state.provider}:notice:${binding.id}:${Number(one["id"])}`));
|
|
733
827
|
if (part !== undefined)
|
|
734
828
|
return Number(part.id);
|
|
735
829
|
}
|
|
@@ -738,7 +832,7 @@ function planResultShots(options, binding, notification, repos, id, now) {
|
|
|
738
832
|
const image = (artifact, sha256) => ({ taskId: where.task, run: where.run, artifact, sha256 });
|
|
739
833
|
const parts = plan.kind === "line" ? [{ text: plan.text, ...where }]
|
|
740
834
|
// Teams has no file upload here: one message that links to the saved result.
|
|
741
|
-
: state.
|
|
835
|
+
: state.provider === "teams" ? [{ text: plan.shots.length === 1 ? plan.shots[0].caption : `${plan.shots[0].caption} · ${plan.shots.length} screenshots`,
|
|
742
836
|
image: image(plan.shots[0].artifact, plan.shots[0].sha256), shot: { follows: null }, ...where }]
|
|
743
837
|
: plan.shots.map(one => ({ text: one.caption, image: image(one.artifact, one.sha256), shot: { follows }, ...where }));
|
|
744
838
|
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
@@ -760,163 +854,187 @@ export async function planChatNotifications(options) {
|
|
|
760
854
|
catch { /* Retried on the next pass. */ }
|
|
761
855
|
enqueueLeadLapses(store, nowOf(options));
|
|
762
856
|
const cursor = Number(state
|
|
763
|
-
.prepare("SELECT notification FROM chat_runtime WHERE installation=?")
|
|
857
|
+
.prepare("SELECT notification FROM chat_runtime WHERE provider=:provider AND installation=?")
|
|
764
858
|
.get(identity.installation)?.notification ?? 0);
|
|
859
|
+
// A person who paired after the cursor passed is still owed what wanted them before: once per pairing, their
|
|
860
|
+
// unresolved attention notices and open decisions from before it, through the same door as every new one.
|
|
861
|
+
for (const binding of bindings) {
|
|
862
|
+
const owed = `owed-before-pairing:${binding.id}`;
|
|
863
|
+
if (state.meta(identity.installation, owed) !== null)
|
|
864
|
+
continue;
|
|
865
|
+
for (const notification of store.notificationsWantedBefore(binding.created, cursor))
|
|
866
|
+
offerNotice(options, notification, binding, registry);
|
|
867
|
+
state.setMeta(identity.installation, owed, "offered", nowOf(options));
|
|
868
|
+
}
|
|
765
869
|
// Every paired person is a destination of their own, under their own ceiling.
|
|
766
870
|
for (const notification of store.notificationsAfter(cursor, 100)) {
|
|
767
|
-
for (const binding of bindings)
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
871
|
+
for (const binding of bindings)
|
|
872
|
+
offerNotice(options, notification, binding, registry);
|
|
873
|
+
state
|
|
874
|
+
.prepare("UPDATE chat_runtime SET notification=? WHERE provider=:provider AND installation=?")
|
|
875
|
+
.run(notification.id, identity.installation);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
/** One notification for one paired person: planned as their parts, or nothing. Never twice: each plan's event id is
|
|
879
|
+
* the notification's for this binding. */
|
|
880
|
+
function offerNotice(options, notification, binding, registry) {
|
|
881
|
+
const { state, store, identity } = options;
|
|
882
|
+
const repos = channelRepos(store, binding.approver, registry);
|
|
883
|
+
store.transact(() => {
|
|
884
|
+
// One person's notification (v83) goes to that person alone, task or not.
|
|
885
|
+
const personal = notification.recipient !== null;
|
|
886
|
+
if (
|
|
887
|
+
// From before the pairing, only what still wants a person (the rule every chat app shares, wantedBeforePairing).
|
|
888
|
+
(notification.createdAt >= binding.created || wantedBeforePairing(notification)) &&
|
|
889
|
+
notification.resolvedAt === null &&
|
|
890
|
+
// A promise the lead made on another chat is reported there (lead-commitments.ts); one its owner asked for on
|
|
891
|
+
// this chat is always said here, however quiet the lead's own work is kept.
|
|
892
|
+
(promiseChannelOf(notification) ?? state.provider) === state.provider &&
|
|
893
|
+
// Pings follow responsibility: the lead's work, this person's own act and a muted project stay in the console.
|
|
894
|
+
(promiseChannelOf(notification) === state.provider || store.pingAllowed(notification, binding.approver)) &&
|
|
895
|
+
// A flow decision for "anyone who approves" reaches every approver who can see the project.
|
|
896
|
+
// A notification addressed to this person reaches them whatever project
|
|
897
|
+
// their channel follows (a sign-in pause is the installation's, v108).
|
|
898
|
+
(personal
|
|
899
|
+
? notification.recipient === binding.approver && (notification.project === null || repos.includes(notification.project))
|
|
900
|
+
: (notification.taskId || notification.kind === "flow-decision") && notification.project !== null && repos.includes(notification.project))) {
|
|
901
|
+
const run = store.telegramProgressRun(notification);
|
|
902
|
+
const id = chatHash(`${options.state.provider}:notice:${binding.id}:${notification.id}`), now = nowOf(options);
|
|
903
|
+
// Only when I'm needed (the default): the task's one card is edited in place, and a new message
|
|
904
|
+
// follows only when this person is needed. Every step keeps the branches below unchanged.
|
|
905
|
+
const quiet = !personal && isTaskFact(notification) && store.notificationPreference(binding.approver).mode === "quiet";
|
|
906
|
+
if (isShotsKind(notification.kind)) {
|
|
907
|
+
planResultShots(options, binding, notification, repos, id, now);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (notification.kind === LEAD_SAY_KIND) {
|
|
911
|
+
// The lead's words (lead-voice.ts): one message, repainted in place when a later say joins it.
|
|
912
|
+
const content = { text: leadSayText(store, notification, binding.approver),
|
|
913
|
+
...(notification.link ? { link: { label: "Open", path: notification.link } } : {}) };
|
|
914
|
+
const earlier = leadSayEarlier(store, notification).map(one => state.prepare("SELECT id FROM chat_part WHERE provider=:provider AND event=?")
|
|
915
|
+
.get(chatHash(`${options.state.provider}:notice:${binding.id}:${one}`))).find(one => one !== undefined);
|
|
916
|
+
if (earlier !== undefined) {
|
|
917
|
+
state.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE provider=:provider AND id=? AND state!='dropped'").run(savedChatPart(content), now.toISOString(), Number(earlier.id));
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
921
|
+
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
922
|
+
state.plan(id, [content], now);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
if (quiet) {
|
|
926
|
+
planQuietCard(options, binding, notification, now);
|
|
927
|
+
if (!needsPerson(notification))
|
|
928
|
+
return;
|
|
929
|
+
if (notification.kind === "run-finished" && run) {
|
|
930
|
+
// Finished work goes out only as the batch line; one left out on purpose (a release check, a replaced
|
|
931
|
+
// task, the reader's own completion) stays quiet rather than falling through to another message.
|
|
932
|
+
planFinished(options, binding, notification.taskRef, run.id, now, null, repos);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
// Every other update for this person within two minutes joins the same one message.
|
|
936
|
+
else if (notification.kind !== "run-finished" && joinsBatch(notification)) {
|
|
937
|
+
if (planFinished(options, binding, notification.taskRef, run?.id ?? null, now, notification.id, repos))
|
|
807
938
|
return;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (!quiet && run && notification.taskId && notification.project !== null && isTelegramProgressNotification(notification)) {
|
|
942
|
+
const card = telegramProgressCard(store, store.getRun(run.id), notification.taskId, notification.project, now, options.evidenceRoot, binding.approver);
|
|
943
|
+
const content = {
|
|
944
|
+
text: card.text,
|
|
945
|
+
task: notification.taskId,
|
|
946
|
+
run: run.id,
|
|
947
|
+
link: card.link,
|
|
948
|
+
};
|
|
949
|
+
const digest = chatHash(JSON.stringify(content));
|
|
950
|
+
const prior = state
|
|
951
|
+
.prepare("SELECT part,digest FROM chat_progress WHERE provider=:provider AND binding=? AND run=?")
|
|
952
|
+
.get(binding.id, run.id);
|
|
953
|
+
if (prior) {
|
|
954
|
+
if (prior.digest !== digest) {
|
|
955
|
+
state
|
|
956
|
+
.prepare("UPDATE chat_part SET payload=?,state='pending',created=?,next_at=NULL WHERE provider=:provider AND id=? AND state!='dropped'")
|
|
957
|
+
.run(JSON.stringify(content), now.toISOString(), Number(prior.part));
|
|
958
|
+
state
|
|
959
|
+
.prepare("UPDATE chat_progress SET digest=? WHERE provider=:provider AND binding=? AND run=?")
|
|
960
|
+
.run(digest, binding.id, run.id);
|
|
808
961
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
881
|
-
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
882
|
-
state.plan(id, flowSendParts(store, notification, state.channel), now);
|
|
883
|
-
}
|
|
884
|
-
else if (notification.kind === "flow-card" && questionParts(store, notification, binding) !== null) {
|
|
885
|
-
// A teammate's question (v93): its options and "Answer in words" on the notice, for the person it asks.
|
|
886
|
-
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
887
|
-
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
888
|
-
state.plan(id, questionParts(store, notification, binding), now);
|
|
889
|
-
}
|
|
890
|
-
else if (notification.pushClass !== null || personal || quiet) {
|
|
891
|
-
state.enqueue({
|
|
892
|
-
id,
|
|
893
|
-
installation: identity.installation,
|
|
894
|
-
binding: binding.id,
|
|
895
|
-
kind: "notice",
|
|
896
|
-
channel: binding.channel,
|
|
897
|
-
member: binding.member,
|
|
898
|
-
ts: "",
|
|
899
|
-
thread: "",
|
|
900
|
-
payload: {},
|
|
901
|
-
created: now.toISOString(),
|
|
902
|
-
});
|
|
903
|
-
state.plan(id, [
|
|
904
|
-
{
|
|
905
|
-
// Never the task's id or a "— revision" suffix (chat-voice.ts).
|
|
906
|
-
text: chatText(phoneText(notification.body === "" ? leadSubjectOf(store, notification, binding.approver) : `${leadSubjectOf(store, notification, binding.approver)}\n\n${notification.body}`, 2500), notification.taskId === null ? [] : [{ id: notification.taskId, title: chatTitle(store, notification.taskId) }]),
|
|
907
|
-
...(notification.taskId ? { task: notification.taskId } : {}),
|
|
908
|
-
...(run ? { run: run.id } : {}),
|
|
909
|
-
...(notification.link
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
state.enqueue({
|
|
965
|
+
id,
|
|
966
|
+
installation: identity.installation,
|
|
967
|
+
binding: binding.id,
|
|
968
|
+
kind: "notice",
|
|
969
|
+
channel: binding.channel,
|
|
970
|
+
member: binding.member,
|
|
971
|
+
ts: "",
|
|
972
|
+
thread: "",
|
|
973
|
+
payload: {},
|
|
974
|
+
created: now.toISOString(),
|
|
975
|
+
});
|
|
976
|
+
state.plan(id, [content], now);
|
|
977
|
+
const part = state
|
|
978
|
+
.prepare("SELECT id FROM chat_part WHERE provider=:provider AND event=?")
|
|
979
|
+
.get(id);
|
|
980
|
+
state
|
|
981
|
+
.prepare("INSERT INTO chat_progress (provider,binding,run,part,digest) VALUES(:provider,?,?,?,?)")
|
|
982
|
+
.run(binding.id, run.id, Number(part.id), digest);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
else if (notification.kind === "flow-decision") {
|
|
986
|
+
// The draft as written, and Approve / Edit / Send back on its last part; a card that moved on is not news.
|
|
987
|
+
const parts = flowDecisionParts(store, notification, state.provider);
|
|
988
|
+
if (parts !== null) {
|
|
989
|
+
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
990
|
+
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
991
|
+
state.plan(id, parts, now);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
else if (personal && flowSendParts(store, notification, state.provider) !== null) {
|
|
995
|
+
// A flow's "Send to me" (flow-send.ts): what was done, with its links as buttons.
|
|
996
|
+
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
997
|
+
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
998
|
+
state.plan(id, flowSendParts(store, notification, state.provider), now);
|
|
999
|
+
}
|
|
1000
|
+
else if (notification.kind === "flow-card" && questionParts(store, notification, binding) !== null) {
|
|
1001
|
+
// A teammate's question (v93): its options and "Answer in words" on the notice, for the person it asks.
|
|
1002
|
+
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
1003
|
+
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
1004
|
+
state.plan(id, questionParts(store, notification, binding), now);
|
|
1005
|
+
}
|
|
1006
|
+
else if (notification.pushClass !== null || personal || quiet) {
|
|
1007
|
+
state.enqueue({
|
|
1008
|
+
id,
|
|
1009
|
+
installation: identity.installation,
|
|
1010
|
+
binding: binding.id,
|
|
1011
|
+
kind: "notice",
|
|
1012
|
+
channel: binding.channel,
|
|
1013
|
+
member: binding.member,
|
|
1014
|
+
ts: "",
|
|
1015
|
+
thread: "",
|
|
1016
|
+
payload: {},
|
|
1017
|
+
created: now.toISOString(),
|
|
1018
|
+
});
|
|
1019
|
+
// A result, plan, failure or ready pull request acts in place (chat-decide.ts); otherwise its one link (a plan's
|
|
1020
|
+
// is Review & start, where the password step lives).
|
|
1021
|
+
const target = decideTargetOf(notification);
|
|
1022
|
+
const offer = decideOfferFor(options, binding, target, repos, now);
|
|
1023
|
+
const fallback = offer === null ? decideFallbackLink(target) : null;
|
|
1024
|
+
state.plan(id, [
|
|
1025
|
+
{
|
|
1026
|
+
// Never the task's id or a "— revision" suffix (chat-voice.ts).
|
|
1027
|
+
text: offer?.text ?? chatText(phoneText(notification.body === "" ? leadSubjectOf(store, notification, binding.approver) : `${leadSubjectOf(store, notification, binding.approver)}\n\n${notification.body}`, 2500), notification.taskId === null ? [] : [{ id: notification.taskId, title: chatTitle(store, notification.taskId) }]),
|
|
1028
|
+
...(notification.taskId ? { task: notification.taskId } : {}),
|
|
1029
|
+
...(run ? { run: run.id } : {}),
|
|
1030
|
+
...(offer !== null ? { decide: mintDecide(options, binding, target, offer, now) }
|
|
1031
|
+
: fallback !== null ? { link: fallback }
|
|
1032
|
+
: notification.link
|
|
910
1033
|
? { link: { label: notification.kind === "pull-request-ready" ? "Merge" : personal ? "Open" : "Review", path: notification.link } }
|
|
911
1034
|
: {}),
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}
|
|
916
|
-
});
|
|
1035
|
+
},
|
|
1036
|
+
], now);
|
|
1037
|
+
}
|
|
917
1038
|
}
|
|
918
|
-
|
|
919
|
-
.prepare("UPDATE chat_runtime SET notification=? WHERE installation=?")
|
|
920
|
-
.run(notification.id, identity.installation);
|
|
921
|
-
}
|
|
1039
|
+
});
|
|
922
1040
|
}
|