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