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/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,7 +630,7 @@ 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
636
|
deferSignal: (signal) => signals.push(signal),
|
|
@@ -566,20 +652,21 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
566
652
|
}
|
|
567
653
|
// Repaint the original persisted card; retain one placement and fresh tokens.
|
|
568
654
|
state
|
|
569
|
-
.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=?")
|
|
570
656
|
.run(savedChatPart(content), action.part);
|
|
571
657
|
if (!content.proposal)
|
|
572
658
|
state
|
|
573
|
-
.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")
|
|
574
660
|
.run(now.toISOString(), proposal.id);
|
|
575
661
|
else
|
|
576
662
|
state
|
|
577
|
-
.prepare("UPDATE chat_action SET consumed=? WHERE token=?")
|
|
663
|
+
.prepare("UPDATE chat_action SET consumed=? WHERE provider=:provider AND token=?")
|
|
578
664
|
.run(now.toISOString(), token);
|
|
579
665
|
state.finish(event.id);
|
|
580
666
|
});
|
|
581
667
|
for (const signal of signals)
|
|
582
668
|
signal();
|
|
669
|
+
return merging;
|
|
583
670
|
}
|
|
584
671
|
/**
|
|
585
672
|
* Carry new conversation messages to every room that follows one: the
|
|
@@ -609,7 +696,7 @@ export async function planRoomMessages(options) {
|
|
|
609
696
|
if (row === null)
|
|
610
697
|
continue;
|
|
611
698
|
for (const message of roomMessagesAfter(store, row.thread, room.cursor)) {
|
|
612
|
-
const text = roomMessageText(message, state.
|
|
699
|
+
const text = roomMessageText(message, state.provider, room.chat);
|
|
613
700
|
let carrier = null;
|
|
614
701
|
if (room.kind === "private")
|
|
615
702
|
carrier = live.find(one => one.channel === room.chat) ?? null;
|
|
@@ -631,7 +718,7 @@ export async function planRoomMessages(options) {
|
|
|
631
718
|
if (message.role === "assistant" && message.turn !== null)
|
|
632
719
|
for (const proposal of store.listMateProposals(row.thread, ["pending"]).filter(one => one.turn === message.turn))
|
|
633
720
|
parts.push({ text: "", proposal: proposal.id, channel: room.chat });
|
|
634
|
-
const id = chatHash(`${state.
|
|
721
|
+
const id = chatHash(`${state.provider}:room:${room.id}:${message.id}`);
|
|
635
722
|
const target = carrier;
|
|
636
723
|
store.transact(() => {
|
|
637
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() }))
|
|
@@ -647,7 +734,7 @@ function planQuietCard(options, binding, notification, now) {
|
|
|
647
734
|
const { state, store, identity } = options;
|
|
648
735
|
if (notification.taskRef === null || notification.taskId === null)
|
|
649
736
|
return;
|
|
650
|
-
const card = store.chatCardFor(`${state.
|
|
737
|
+
const card = store.chatCardFor(`${state.provider}:${binding.id}`, notification.taskRef, store.getTask(notification.taskId)?.createdAt ?? notification.createdAt, now);
|
|
651
738
|
const view = quietCardView(store, card.tasks, now, options.evidenceRoot, binding.approver);
|
|
652
739
|
if (view === null)
|
|
653
740
|
return;
|
|
@@ -656,45 +743,53 @@ function planQuietCard(options, binding, notification, now) {
|
|
|
656
743
|
if (card.message !== null) {
|
|
657
744
|
if (card.digest === shown)
|
|
658
745
|
return;
|
|
659
|
-
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));
|
|
660
747
|
store.setChatCardMessage(card.id, card.message, shown);
|
|
661
748
|
return;
|
|
662
749
|
}
|
|
663
|
-
const id = chatHash(`${state.
|
|
750
|
+
const id = chatHash(`${state.provider}:card:${binding.id}:${card.id}`);
|
|
664
751
|
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
665
752
|
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
666
753
|
state.plan(id, [content], now);
|
|
667
|
-
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);
|
|
668
755
|
if (part !== undefined)
|
|
669
756
|
store.setChatCardMessage(card.id, String(part.id), shown);
|
|
670
757
|
}
|
|
671
758
|
/** Any update for this person joins their open batch (two minutes from its first), and the batch's one message
|
|
672
759
|
* is planned once, then repainted in place as more updates land. `notification`: an update that is not a finished
|
|
673
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. */
|
|
674
|
-
function planFinished(options, binding, taskRef, run, now, notification = null) {
|
|
761
|
+
function planFinished(options, binding, taskRef, run, now, notification = null, repos = []) {
|
|
675
762
|
const { state, store, identity } = options;
|
|
676
|
-
const batch = store.chatBatchFor(`${state.
|
|
763
|
+
const batch = store.chatBatchFor(`${state.provider}:${binding.id}`, taskRef, run, now, BATCH_MS, notification);
|
|
677
764
|
const view = finishedView(store, batch, now, options.evidenceRoot, binding.approver);
|
|
678
765
|
if (view === null)
|
|
679
766
|
return false;
|
|
680
767
|
// A single update keeps its task and run, so a reply to it names that work.
|
|
681
768
|
const single = batch.items.length === 1 ? batch.items[0] : null;
|
|
682
769
|
const task = single === null ? null : store.refById(single.taskRef)?.externalId ?? null;
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
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)]));
|
|
686
778
|
if (batch.message !== null) {
|
|
687
|
-
|
|
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)
|
|
688
782
|
return true;
|
|
689
|
-
|
|
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));
|
|
690
785
|
store.setChatBatchMessage(batch.id, batch.message, shown);
|
|
691
786
|
return true;
|
|
692
787
|
}
|
|
693
|
-
const id = chatHash(`${state.
|
|
788
|
+
const id = chatHash(`${state.provider}:batch:${binding.id}:${batch.id}`);
|
|
694
789
|
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
695
790
|
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
696
|
-
state.plan(id, [content], now);
|
|
697
|
-
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);
|
|
698
793
|
if (part !== undefined)
|
|
699
794
|
store.setChatBatchMessage(batch.id, String(part.id), shown);
|
|
700
795
|
return true;
|
|
@@ -715,20 +810,20 @@ function planResultShots(options, binding, notification, repos, id, now) {
|
|
|
715
810
|
if (visit !== null) {
|
|
716
811
|
for (const key of [`flow-send:${visit[1]}:${visit[2]}`, `flow-choose:${visit[1]}:${visit[2]}`]) {
|
|
717
812
|
const notice = store.handle.prepare("SELECT id FROM notification WHERE dedupe_key=?").get(key);
|
|
718
|
-
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"])}`));
|
|
719
814
|
if (part !== undefined)
|
|
720
815
|
return Number(part.id);
|
|
721
816
|
}
|
|
722
817
|
return null;
|
|
723
818
|
}
|
|
724
|
-
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);
|
|
725
820
|
if (batch !== null)
|
|
726
821
|
return Number(batch);
|
|
727
|
-
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);
|
|
728
823
|
if (card !== undefined)
|
|
729
824
|
return Number(card.part);
|
|
730
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)) {
|
|
731
|
-
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"])}`));
|
|
732
827
|
if (part !== undefined)
|
|
733
828
|
return Number(part.id);
|
|
734
829
|
}
|
|
@@ -737,7 +832,7 @@ function planResultShots(options, binding, notification, repos, id, now) {
|
|
|
737
832
|
const image = (artifact, sha256) => ({ taskId: where.task, run: where.run, artifact, sha256 });
|
|
738
833
|
const parts = plan.kind === "line" ? [{ text: plan.text, ...where }]
|
|
739
834
|
// Teams has no file upload here: one message that links to the saved result.
|
|
740
|
-
: state.
|
|
835
|
+
: state.provider === "teams" ? [{ text: plan.shots.length === 1 ? plan.shots[0].caption : `${plan.shots[0].caption} · ${plan.shots.length} screenshots`,
|
|
741
836
|
image: image(plan.shots[0].artifact, plan.shots[0].sha256), shot: { follows: null }, ...where }]
|
|
742
837
|
: plan.shots.map(one => ({ text: one.caption, image: image(one.artifact, one.sha256), shot: { follows }, ...where }));
|
|
743
838
|
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
@@ -759,163 +854,187 @@ export async function planChatNotifications(options) {
|
|
|
759
854
|
catch { /* Retried on the next pass. */ }
|
|
760
855
|
enqueueLeadLapses(store, nowOf(options));
|
|
761
856
|
const cursor = Number(state
|
|
762
|
-
.prepare("SELECT notification FROM chat_runtime WHERE installation=?")
|
|
857
|
+
.prepare("SELECT notification FROM chat_runtime WHERE provider=:provider AND installation=?")
|
|
763
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
|
+
}
|
|
764
869
|
// Every paired person is a destination of their own, under their own ceiling.
|
|
765
870
|
for (const notification of store.notificationsAfter(cursor, 100)) {
|
|
766
|
-
for (const binding of bindings)
|
|
767
|
-
|
|
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
|
-
|
|
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))
|
|
806
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);
|
|
807
961
|
}
|
|
808
|
-
|
|
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
|
-
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
880
|
-
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
881
|
-
state.plan(id, flowSendParts(store, notification, state.channel), now);
|
|
882
|
-
}
|
|
883
|
-
else if (notification.kind === "flow-card" && questionParts(store, notification, binding) !== null) {
|
|
884
|
-
// A teammate's question (v93): its options and "Answer in words" on the notice, for the person it asks.
|
|
885
|
-
state.enqueue({ id, installation: identity.installation, binding: binding.id, kind: "notice", channel: binding.channel, member: binding.member,
|
|
886
|
-
ts: "", thread: "", payload: {}, created: now.toISOString() });
|
|
887
|
-
state.plan(id, questionParts(store, notification, binding), now);
|
|
888
|
-
}
|
|
889
|
-
else if (notification.pushClass !== null || personal || quiet) {
|
|
890
|
-
state.enqueue({
|
|
891
|
-
id,
|
|
892
|
-
installation: identity.installation,
|
|
893
|
-
binding: binding.id,
|
|
894
|
-
kind: "notice",
|
|
895
|
-
channel: binding.channel,
|
|
896
|
-
member: binding.member,
|
|
897
|
-
ts: "",
|
|
898
|
-
thread: "",
|
|
899
|
-
payload: {},
|
|
900
|
-
created: now.toISOString(),
|
|
901
|
-
});
|
|
902
|
-
state.plan(id, [
|
|
903
|
-
{
|
|
904
|
-
// Never the task's id or a "— revision" suffix (chat-voice.ts).
|
|
905
|
-
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) }]),
|
|
906
|
-
...(notification.taskId ? { task: notification.taskId } : {}),
|
|
907
|
-
...(run ? { run: run.id } : {}),
|
|
908
|
-
...(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
|
|
909
1033
|
? { link: { label: notification.kind === "pull-request-ready" ? "Merge" : personal ? "Open" : "Review", path: notification.link } }
|
|
910
1034
|
: {}),
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
}
|
|
915
|
-
});
|
|
1035
|
+
},
|
|
1036
|
+
], now);
|
|
1037
|
+
}
|
|
916
1038
|
}
|
|
917
|
-
|
|
918
|
-
.prepare("UPDATE chat_runtime SET notification=? WHERE installation=?")
|
|
919
|
-
.run(notification.id, identity.installation);
|
|
920
|
-
}
|
|
1039
|
+
});
|
|
921
1040
|
}
|