toolroll 0.9.34 → 0.9.37
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 +102 -0
- package/README.md +7 -0
- package/dist/assignment-adapters.d.ts +12 -48
- package/dist/assignment-adapters.js +33 -43
- package/dist/assignment.d.ts +1 -0
- package/dist/assignment.js +2 -2
- package/dist/browser-workspace.d.ts +3 -3
- package/dist/build-review.d.ts +1 -1
- package/dist/build-review.js +2 -1
- package/dist/builder.js +4 -7
- package/dist/chat-acceptance.d.ts +2 -2
- package/dist/chat-actions.d.ts +18 -12
- package/dist/chat-actions.js +15 -65
- package/dist/chat-ask.js +5 -5
- package/dist/chat-decide.d.ts +14 -1
- package/dist/chat-decide.js +12 -6
- package/dist/chat-delivery-state.d.ts +12 -70
- package/dist/chat-delivery-state.js +17 -135
- package/dist/chat-delivery.js +58 -30
- package/dist/chat-flow.js +8 -6
- package/dist/chat-inbox.js +1 -1
- package/dist/coding-context.d.ts +2 -39
- package/dist/coding-context.js +2 -1
- package/dist/coding-handoff.d.ts +6 -25
- package/dist/coding-handoff.js +11 -5
- package/dist/coding-types.d.ts +2 -26
- package/dist/coding-workspace.d.ts +2 -0
- package/dist/coding-workspace.js +50 -14
- package/dist/contracts/acceptance-terms.d.ts +14 -0
- package/dist/contracts/acceptance-terms.js +16 -0
- package/dist/contracts/chat-actions.d.ts +328 -0
- package/dist/contracts/chat-actions.js +133 -0
- package/dist/contracts/chat-callback-rows.d.ts +59 -0
- package/dist/contracts/chat-callback-rows.js +46 -0
- package/dist/contracts/chat-content.d.ts +276 -0
- package/dist/contracts/chat-content.js +127 -0
- package/dist/contracts/chat-tables.d.ts +6 -0
- package/dist/contracts/chat-tables.js +136 -0
- package/dist/contracts/coding-context.d.ts +97 -0
- package/dist/contracts/coding-context.js +60 -0
- package/dist/contracts/coding-handoff.d.ts +44 -0
- package/dist/contracts/coding-handoff.js +47 -0
- package/dist/contracts/coding-workspace.d.ts +165 -0
- package/dist/contracts/coding-workspace.js +71 -0
- package/dist/contracts/contract.d.ts +40 -7
- package/dist/contracts/contract.js +155 -19
- package/dist/contracts/decision.d.ts +45 -0
- package/dist/contracts/decision.js +45 -0
- package/dist/contracts/discord-callback.d.ts +45 -0
- package/dist/contracts/discord-callback.js +34 -0
- package/dist/contracts/flow-card.d.ts +50 -0
- package/dist/contracts/flow-card.js +46 -0
- package/dist/contracts/flow-propose.d.ts +325 -0
- package/dist/contracts/flow-propose.js +41 -0
- package/dist/contracts/flow-send.d.ts +119 -0
- package/dist/contracts/flow-send.js +112 -0
- package/dist/contracts/flow.d.ts +19405 -0
- package/dist/contracts/flow.js +364 -0
- package/dist/contracts/gateway-tools.d.ts +374 -0
- package/dist/contracts/gateway-tools.js +94 -0
- package/dist/contracts/handoff.d.ts +91 -0
- package/dist/contracts/handoff.js +82 -0
- package/dist/contracts/lead-context.d.ts +165 -0
- package/dist/contracts/lead-context.js +73 -0
- package/dist/contracts/lead-tools.d.ts +1905 -0
- package/dist/contracts/lead-tools.js +265 -0
- package/dist/contracts/memory-pass.d.ts +158 -0
- package/dist/contracts/memory-pass.js +102 -0
- package/dist/contracts/project-knowledge.d.ts +126 -0
- package/dist/contracts/project-knowledge.js +93 -0
- package/dist/contracts/project-memory.d.ts +117 -0
- package/dist/contracts/project-memory.js +51 -0
- package/dist/contracts/project-skills.d.ts +121 -0
- package/dist/contracts/project-skills.js +76 -0
- package/dist/contracts/proof.d.ts +136 -0
- package/dist/contracts/proof.js +110 -0
- package/dist/contracts/review-findings.d.ts +46 -0
- package/dist/contracts/review-findings.js +48 -0
- package/dist/contracts/route.d.ts +484 -0
- package/dist/contracts/route.js +128 -0
- package/dist/contracts/scope.d.ts +105 -0
- package/dist/contracts/scope.js +114 -0
- package/dist/contracts/scout-report.d.ts +234 -0
- package/dist/contracts/scout-report.js +141 -0
- package/dist/contracts/slack-callback.d.ts +43 -0
- package/dist/contracts/slack-callback.js +35 -0
- package/dist/contracts/sort-answer.d.ts +83 -0
- package/dist/contracts/sort-answer.js +77 -0
- package/dist/contracts/stage-output.d.ts +127 -0
- package/dist/contracts/stage-output.js +166 -0
- package/dist/contracts/task-sizing.d.ts +24 -0
- package/dist/contracts/task-sizing.js +28 -0
- package/dist/contracts/teammate-turn.d.ts +43 -0
- package/dist/contracts/teammate-turn.js +51 -0
- package/dist/contracts/teams-callback.d.ts +14 -0
- package/dist/contracts/teams-callback.js +14 -0
- package/dist/contracts/telegram-callback.d.ts +120 -0
- package/dist/contracts/telegram-callback.js +97 -0
- package/dist/contracts/verification-receipt.d.ts +148 -0
- package/dist/contracts/verification-receipt.js +87 -0
- package/dist/decision.d.ts +11 -20
- package/dist/decision.js +86 -167
- package/dist/demo.js +28 -11
- package/dist/discord-chat.js +31 -20
- package/dist/evidence.d.ts +3 -40
- package/dist/evidence.js +5 -1
- package/dist/flow-engine.js +14 -17
- package/dist/flow-gallery.d.ts +3 -2
- package/dist/flow-items.d.ts +4 -13
- package/dist/flow-items.js +1 -15
- package/dist/flow-send.d.ts +6 -37
- package/dist/flow-send.js +18 -14
- package/dist/flow-share.d.ts +10 -32
- package/dist/flow-share.js +72 -84
- package/dist/flow-sort.d.ts +7 -26
- package/dist/flow-sort.js +25 -20
- package/dist/flow-starters.d.ts +2 -1
- package/dist/flow-steps.js +3 -2
- package/dist/flow-triggers.d.ts +15 -72
- package/dist/flow-triggers.js +94 -83
- package/dist/flows.d.ts +37 -220
- package/dist/flows.js +356 -387
- package/dist/invoke.js +4 -4
- package/dist/lead-context.d.ts +4 -2
- package/dist/lead-context.js +24 -17
- package/dist/mate-tools.d.ts +4 -0
- package/dist/mate-tools.js +111 -308
- package/dist/mcp-connect.d.ts +27 -4
- package/dist/mcp-connect.js +92 -17
- package/dist/mcp.js +84 -216
- package/dist/memory-pass.d.ts +17 -42
- package/dist/memory-pass.js +54 -26
- package/dist/operate.js +31 -2
- package/dist/phase-routing.d.ts +19 -72
- package/dist/phase-routing.js +34 -164
- package/dist/plan.d.ts +5 -5
- package/dist/plan.js +5 -25
- package/dist/process-recovery-eligibility.d.ts +2 -2
- package/dist/process-recovery-eligibility.js +13 -8
- package/dist/project-knowledge.d.ts +16 -35
- package/dist/project-knowledge.js +42 -21
- package/dist/project-memory.d.ts +2 -28
- package/dist/project-memory.js +28 -13
- package/dist/project-skills.d.ts +16 -38
- package/dist/project-skills.js +41 -32
- package/dist/project-tools.d.ts +1 -0
- package/dist/project-tools.js +4 -4
- package/dist/proof.d.ts +15 -91
- package/dist/proof.js +90 -249
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +4 -16
- package/dist/result-evidence-readers.d.ts +42 -0
- package/dist/result-evidence-readers.js +91 -0
- package/dist/result-shots.d.ts +1 -1
- package/dist/result-shots.js +2 -1
- package/dist/review-switch.d.ts +3 -8
- package/dist/reviewer.d.ts +8 -12
- package/dist/reviewer.js +11 -35
- package/dist/scope.d.ts +21 -43
- package/dist/scope.js +42 -128
- package/dist/scout-report.d.ts +9 -194
- package/dist/scout-report.js +113 -264
- package/dist/scout.js +15 -11
- package/dist/serve.js +17 -101
- package/dist/slack-chat.js +26 -20
- package/dist/slack-state.js +2 -1
- package/dist/store.d.ts +9 -32
- package/dist/store.js +18 -15
- package/dist/structured-output.js +2 -1
- package/dist/subscription-chat.d.ts +4 -0
- package/dist/subscription-chat.js +4 -1
- package/dist/surface.js +8 -1
- package/dist/task-review-brief.d.ts +69 -0
- package/dist/task-review-brief.js +179 -0
- package/dist/task-sizing.d.ts +3 -27
- package/dist/task-sizing.js +11 -26
- package/dist/task-text.d.ts +0 -5
- package/dist/task-text.js +1 -3
- package/dist/teammate-question.js +7 -5
- package/dist/teammate-work.js +9 -5
- package/dist/teammates.d.ts +13 -64
- package/dist/teammates.js +32 -33
- package/dist/teams-chat.js +20 -11
- package/dist/telegram-flow.js +4 -3
- package/dist/telegram-mate.d.ts +4 -14
- package/dist/telegram-mate.js +8 -7
- package/dist/telegram-team.d.ts +2 -19
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +52 -42
- package/dist/text-limits.d.ts +212 -1
- package/dist/text-limits.js +214 -1
- package/dist/tools-ui.d.ts +2 -1
- package/dist/tools-ui.js +7 -4
- package/dist/verification-evidence.js +24 -12
- package/package.json +1 -1
package/dist/teammates.js
CHANGED
|
@@ -24,6 +24,7 @@ import { strictJsonParse } from "./converse.js";
|
|
|
24
24
|
import { scanForSecrets } from "./evidence.js";
|
|
25
25
|
import { run } from "./exec.js";
|
|
26
26
|
import { ALL_CREDENTIAL_ENV } from "./provider.js";
|
|
27
|
+
import { readTurnAnswer, TURN_MODEL_SCHEMA } from "./contracts/teammate-turn.js";
|
|
27
28
|
import { LIMITS } from "./decision.js";
|
|
28
29
|
import { overruns, TEXT_LIMITS } from "./text-limits.js";
|
|
29
30
|
export const SOUL_CHARS = 12_000;
|
|
@@ -121,23 +122,13 @@ export const TEAMMATE_TEMPLATES = [
|
|
|
121
122
|
];
|
|
122
123
|
/** Each field's limit, in characters: stated in the prompt before it answers (turnPrompt), and an answer over one is
|
|
123
124
|
* asked once to shorten (teammateTurn); one still over is kept whole, never cut. "text" is what the next zones read;
|
|
124
|
-
* "note" is a decision or send-back note.
|
|
125
|
-
|
|
125
|
+
* "note" is a decision or send-back note. Not in the turn's schema (src/contracts/teammate-turn.ts): the CLI would
|
|
126
|
+
* refuse an answer a few characters over one whole. */
|
|
127
|
+
export const TURN_LIMITS = { answer: TEXT_LIMITS.teammateAnswer, text: TEXT_LIMITS.stageOutput, note: LIMITS.note, question: TEXT_LIMITS.teammateQuestion, reason: TEXT_LIMITS.teammateReason, remember: TEXT_LIMITS.teammateRemember };
|
|
126
128
|
/** The fields of an answer over their limits. */
|
|
127
129
|
export function turnOverruns(answer) {
|
|
128
130
|
return overruns({ answer: answer.answer, text: answer.text, note: answer.note, question: answer.question, reason: answer.reason, remember: answer.remember }, TURN_LIMITS);
|
|
129
131
|
}
|
|
130
|
-
/** The shape Claude answers in: one flat object (a root union is refused). No length limits here: an answer
|
|
131
|
-
* a few characters over one is refused whole by the CLI, so the limits are TURN_LIMITS, checked after. */
|
|
132
|
-
export const TURN_SCHEMA = {
|
|
133
|
-
type: "object", additionalProperties: false,
|
|
134
|
-
required: ["action", "answer", "text", "note", "question", "options", "reason", "tool", "input", "remember"],
|
|
135
|
-
properties: {
|
|
136
|
-
action: { type: "string", enum: ["approve", "send_back", "hand_off", "route", "ask", "cant", "use_tool"] },
|
|
137
|
-
answer: { type: "string" }, text: { type: "string" }, note: { type: "string" }, question: { type: "string" },
|
|
138
|
-
options: { type: "array", items: { type: "string" } }, reason: { type: "string" }, tool: { type: "string" }, input: { type: "string" }, remember: { type: "string" },
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
132
|
const clip = (text, cap) => text.length <= cap ? text : `${text.slice(0, cap - 1)}…`;
|
|
142
133
|
export function turnPrompt(context) {
|
|
143
134
|
const actions = context.kind === "decide"
|
|
@@ -192,41 +183,49 @@ export function turnPrompt(context) {
|
|
|
192
183
|
...(context.shorten === undefined ? [] : ["", "YOUR LAST ANSWER, WHICH IS OVER A LIMIT", context.shorten.answer, "", context.shorten.ask]),
|
|
193
184
|
].join("\n");
|
|
194
185
|
}
|
|
195
|
-
/**
|
|
186
|
+
/** How many options an ask offers, and the longest tool name and input a tool call may give. */
|
|
187
|
+
const ASK_OPTIONS = 4, TOOL_NAME_CHARS = 140, TOOL_INPUT_CHARS = 8000;
|
|
188
|
+
/**
|
|
189
|
+
* A turn's answer, read through its contract (src/contracts/teammate-turn.ts) and then checked against what this zone
|
|
190
|
+
* allows, each refusal naming its path (`action: "route" isn't one this zone allows`). Text is trimmed and kept whole
|
|
191
|
+
* (a field over its limit is asked to shorten, turnOverruns); blank options are dropped, and an ask offers at most 4,
|
|
192
|
+
* each cut to an answer's length, and a tool name is cut to 140, as they always were.
|
|
193
|
+
*/
|
|
196
194
|
export function readTurn(value, context) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
195
|
+
const read = readTurnAnswer(value);
|
|
196
|
+
if (!read.ok)
|
|
197
|
+
return read;
|
|
198
|
+
const raw = read.value;
|
|
199
|
+
const answer = { action: raw.action, answer: raw.answer.trim(), text: raw.text.trim(), note: raw.note.trim(), question: raw.question.trim(),
|
|
200
|
+
options: raw.options.filter(one => one.trim() !== "").map(one => one.trim().slice(0, TURN_LIMITS.answer)).slice(0, ASK_OPTIONS), reason: raw.reason.trim(),
|
|
201
|
+
tool: raw.tool.trim().slice(0, TOOL_NAME_CHARS), input: raw.input.trim(), remember: raw.remember.trim() };
|
|
203
202
|
const allowed = [...(context.kind === "decide" ? ["approve", "hand_off", ...(context.canSendBack ? ["send_back"] : [])] : ["route", "ask", "cant"]),
|
|
204
203
|
...((context.tools ?? []).length > 0 ? ["use_tool"] : [])];
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
options: Array.isArray(raw["options"]) ? raw["options"].filter((one) => typeof one === "string" && one.trim() !== "").map(one => one.trim().slice(0, TURN_LIMITS.answer)).slice(0, 4) : [], reason: text("reason"),
|
|
209
|
-
tool: text("tool").slice(0, 140), input: typeof raw["input"] === "string" ? raw["input"].trim() : "", remember: text("remember") };
|
|
204
|
+
const refuse = (path, what) => ({ ok: false, issues: [{ path, kind: "bad-value", line: `${path}: ${what}` }] });
|
|
205
|
+
if (!allowed.includes(answer.action))
|
|
206
|
+
return refuse("action", `"${answer.action}" isn't one this zone allows (${allowed.map(one => `"${one}"`).join(", ")})`);
|
|
210
207
|
// A tool call is checked against the teammate's rules where it's carried out; here only that it names one and fits.
|
|
211
|
-
if (answer.action === "use_tool" &&
|
|
212
|
-
return
|
|
208
|
+
if (answer.action === "use_tool" && answer.tool === "")
|
|
209
|
+
return refuse("tool", "required for use_tool");
|
|
210
|
+
if (answer.action === "use_tool" && answer.input.length > TOOL_INPUT_CHARS)
|
|
211
|
+
return refuse("input", `at most ${TOOL_INPUT_CHARS.toLocaleString("en-US")} characters`);
|
|
213
212
|
if (answer.action === "route" && !context.answers.some(one => one.toLowerCase() === answer.answer.toLowerCase()))
|
|
214
|
-
return
|
|
213
|
+
return refuse("answer", `"${answer.answer}" isn't one of this zone's answers`);
|
|
215
214
|
if (answer.action === "send_back" && answer.note === "")
|
|
216
|
-
return
|
|
215
|
+
return refuse("note", "required for send_back");
|
|
217
216
|
if (answer.action === "ask" && answer.question === "")
|
|
218
|
-
return
|
|
219
|
-
return answer;
|
|
217
|
+
return refuse("question", "required for ask");
|
|
218
|
+
return { ok: true, value: answer };
|
|
220
219
|
}
|
|
221
220
|
export const TURN_TIMEOUT_MS = 180_000;
|
|
222
|
-
/** The production runner: Claude through this computer's sign-in, answering in
|
|
221
|
+
/** The production runner: Claude through this computer's sign-in, answering in the turn schema (TURN_MODEL_SCHEMA), with no tools, no MCP servers and no repository. */
|
|
223
222
|
export function claudeTurnRunner(runner = run) {
|
|
224
223
|
return async (request) => {
|
|
225
224
|
const dir = mkdtempSync(join(tmpdir(), "standing-orders-teammate-"));
|
|
226
225
|
const started = Date.now();
|
|
227
226
|
try {
|
|
228
227
|
const result = await runner("claude", [
|
|
229
|
-
"-p", "--output-format", "json", "--json-schema", JSON.stringify(
|
|
228
|
+
"-p", "--output-format", "json", "--json-schema", JSON.stringify(TURN_MODEL_SCHEMA), "--safe-mode", "--no-session-persistence", "--tools", "", "--permission-mode", "dontAsk",
|
|
230
229
|
"--permission-prompts", "none", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}',
|
|
231
230
|
...(request.model === "default" ? [] : ["--model", request.model]),
|
|
232
231
|
], { cwd: dir, stdin: request.prompt, timeoutMs: request.timeoutMs, maxBuffer: 512 * 1024, omitEnv: ALL_CREDENTIAL_ENV, processGroup: true });
|
package/dist/teams-chat.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { resultShotsPruned } from "./result-shots.js";
|
|
4
4
|
import { chatQuestionButtons } from "./teammate-question.js";
|
|
5
5
|
import { chatAskButtons } from "./chat-ask.js";
|
|
6
|
-
import { ChatState, chatHash } from "./chat-delivery-state.js";
|
|
6
|
+
import { ChatDeliveryError, ChatState, chatHash, partContent } from "./chat-delivery-state.js";
|
|
7
7
|
import { channelAccess, chatObject as object, planChatNotifications, planRoomMessages, processChatEvent, splitChatText } from "./chat-delivery.js";
|
|
8
8
|
import { PLATFORM_LIMITS } from "./text-limits.js";
|
|
9
9
|
import { roomCommand } from "./chat-rooms.js";
|
|
@@ -14,6 +14,7 @@ import { chatFlowButtons } from "./chat-flow.js";
|
|
|
14
14
|
import { channelInbox } from "./chat-inbox.js";
|
|
15
15
|
import { chatResultHref } from "./chat-controls.js";
|
|
16
16
|
import { TeamsError } from "./teams-api.js";
|
|
17
|
+
import { readTeamsSubmit } from "./contracts/teams-callback.js";
|
|
17
18
|
const serviceKey = (conversation) => `serviceUrl:${conversation}`;
|
|
18
19
|
export const teamsUserId = (v) => typeof v === "string" && /^29:[A-Za-z0-9_=-]{10,200}$/.test(v);
|
|
19
20
|
/** A conversation id; a channel post's carries its thread as ";messageid=<first message>" (v89). */
|
|
@@ -91,13 +92,16 @@ export function receiveTeams(state, identity, raw, serviceUrlClaim, now) {
|
|
|
91
92
|
return false;
|
|
92
93
|
if (!isRoom && conversation.conversationType !== "personal")
|
|
93
94
|
return false;
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
// A card's button: its data is Toolroll's. One that can't be read is answered with why, once the tap is proved to
|
|
96
|
+
// be the paired person's below. A button carries no words of its own: other data beside words is a message, as before.
|
|
97
|
+
const read = activity.value === undefined || activity.value === null ? null : readTeamsSubmit(activity.value);
|
|
98
|
+
const button = read !== null && (read.ok || typeof activity.text !== "string") ? read : null;
|
|
99
|
+
const submitted = button?.ok === true ? button.value : null;
|
|
96
100
|
let kind, payload, eventId;
|
|
97
|
-
if (
|
|
101
|
+
if (button !== null) {
|
|
98
102
|
kind = "action";
|
|
99
|
-
payload = { token:
|
|
100
|
-
eventId = chatHash(`${identity.installation}:action:${member}:${activity.replyToId ?? id}:${submitted}`);
|
|
103
|
+
payload = button.ok ? { token: button.value } : { problem: button.issues.map(issue => issue.line).join("; ") };
|
|
104
|
+
eventId = chatHash(`${identity.installation}:action:${member}:${activity.replyToId ?? id}:${submitted ?? JSON.stringify(activity.value) ?? ""}`);
|
|
101
105
|
}
|
|
102
106
|
else {
|
|
103
107
|
if (typeof activity.text !== "string")
|
|
@@ -109,9 +113,10 @@ export function receiveTeams(state, identity, raw, serviceUrlClaim, now) {
|
|
|
109
113
|
...(Array.isArray(activity.attachments) && activity.attachments.length > 0 ? { unsupported: "Incoming files are not supported yet. Describe the request in a message; saved result screenshots open from their links." } : {}) };
|
|
110
114
|
eventId = chatHash(`${identity.installation}:message:${id}`);
|
|
111
115
|
}
|
|
116
|
+
const words = "text" in payload ? payload.text ?? "" : "";
|
|
112
117
|
// v89: a channel that feeds a flow takes anyone's message (as a card, with no say over anything); "flow 12" connects one.
|
|
113
|
-
const inbox = isRoom && kind === "message" ? channelInbox(state.store, "teams", identity.installation, channel,
|
|
114
|
-
const roomish = isRoom && (state.room(identity.installation, channel) !== null || (kind === "message" && roomCommand(
|
|
118
|
+
const inbox = isRoom && kind === "message" ? channelInbox(state.store, "teams", identity.installation, channel, words) : { watched: false, command: false };
|
|
119
|
+
const roomish = isRoom && (state.room(identity.installation, channel) !== null || (kind === "message" && roomCommand(words) !== null) || inbox.watched || inbox.command);
|
|
115
120
|
if (isRoom && !roomish)
|
|
116
121
|
return false;
|
|
117
122
|
const binding = state.bindingFor(identity.installation, member);
|
|
@@ -122,8 +127,8 @@ export function receiveTeams(state, identity, raw, serviceUrlClaim, now) {
|
|
|
122
127
|
state.setMeta(identity.installation, serviceKey(channel), activity.serviceUrl, now);
|
|
123
128
|
return state.enqueue({
|
|
124
129
|
id: eventId, installation: identity.installation, binding: kind === "pair" || binding === null ? null : binding.id, kind, channel, member,
|
|
125
|
-
ts:
|
|
126
|
-
payload
|
|
130
|
+
ts: kind === "action" ? String(activity.replyToId ?? id) : id, thread: kind === "action" ? String(activity.replyToId ?? id) : id,
|
|
131
|
+
payload, created: now.toISOString(),
|
|
127
132
|
});
|
|
128
133
|
});
|
|
129
134
|
}
|
|
@@ -175,7 +180,7 @@ export async function deliverTeamsPart(options) {
|
|
|
175
180
|
const repos = await channelAccess(shared, binding, session?.ceilingDigest);
|
|
176
181
|
if (event.kind === "notice" && options.canNotify?.() === false)
|
|
177
182
|
return false;
|
|
178
|
-
const content =
|
|
183
|
+
const content = partContent(row.payload);
|
|
179
184
|
const destination = content.channel ?? binding.channel;
|
|
180
185
|
const serviceUrl = state.meta(identity.installation, serviceKey(destination));
|
|
181
186
|
if (serviceUrl === null)
|
|
@@ -237,6 +242,10 @@ export async function deliverTeamsPart(options) {
|
|
|
237
242
|
return true;
|
|
238
243
|
}
|
|
239
244
|
catch (error) {
|
|
245
|
+
if (error instanceof ChatDeliveryError && error.permanent) {
|
|
246
|
+
state.prepare("UPDATE chat_part SET state='dropped',next_at=NULL,problem=? WHERE id=?").run(error.message, row.id);
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
240
249
|
const problem = error instanceof TeamsError ? error : new TeamsError("Teams delivery failed", 15_000, true);
|
|
241
250
|
state.prepare("UPDATE chat_part SET attempts=attempts+1,uncertain=?,next_at=?,problem=?,state=CASE WHEN attempts>=20 THEN 'dropped' ELSE state END WHERE id=?")
|
|
242
251
|
.run(problem.uncertain ? 1 : 0, new Date(now.getTime() + problem.retryMs).toISOString(), problem.message, row.id);
|
package/dist/telegram-flow.js
CHANGED
|
@@ -26,6 +26,7 @@ import { decideFlowCard, draftFor, flowDefinitionOf } from "./flow-engine.js";
|
|
|
26
26
|
import { chooseFlowCard, flowChoiceAt, flowSendPaths, readFlowSend } from "./flow-send.js";
|
|
27
27
|
import { deciderOf } from "./flows.js";
|
|
28
28
|
import { phoneLinkButton } from "./telegram-mate.js";
|
|
29
|
+
import { telegramButton } from "./contracts/telegram-callback.js";
|
|
29
30
|
export const FLOW_DECIDE_KEY = /^flow-decide:([1-9][0-9]{0,14}):([1-9][0-9]{0,9})$/;
|
|
30
31
|
const DRAFT_LIMIT = 4000;
|
|
31
32
|
/** The card, if it is still at that visit, waiting at a decision. */
|
|
@@ -48,9 +49,9 @@ export function flowButtons(store, binding, waiting, now) {
|
|
|
48
49
|
const back = waiting.stage.onFail !== null ? mint("send-back") : null;
|
|
49
50
|
const all = [approve, edit, back].filter((one) => one !== null);
|
|
50
51
|
store.createTelegramFlowActions({ binding: binding.id, chatId: binding.chatId, card: waiting.card.id, entry: waiting.card.entry }, all, now);
|
|
51
|
-
const second = [edit === null ? null :
|
|
52
|
+
const second = [edit === null ? null : telegramButton("✏️ Edit", edit.token), back === null ? null : telegramButton("↩️ Send back", back.token)]
|
|
52
53
|
.filter((one) => one !== null);
|
|
53
|
-
return { keyboard: [[
|
|
54
|
+
return { keyboard: [[telegramButton("✅ Approve", approve.token)], ...(second.length === 0 ? [] : [second])], tokens: all.map(one => one.token) };
|
|
54
55
|
}
|
|
55
56
|
/** What a "Send to me" or "Person chooses" visit sent, read back for its message. */
|
|
56
57
|
export function flowSentContent(store, card, entry) {
|
|
@@ -68,7 +69,7 @@ export function flowChoiceButtons(store, binding, visit, content, now) {
|
|
|
68
69
|
// Only the options the zone still offers as they were sent: a flow changed since sends no stale button.
|
|
69
70
|
const options = (content.options ?? []).filter(one => visit.stage.options?.[one.choice]?.label === one.label).map(one => ({ ...one, token: randomBytes(16).toString("hex") }));
|
|
70
71
|
store.createTelegramFlowChoices({ binding: binding.id, chatId: binding.chatId, card: visit.card.id, entry: visit.card.entry }, options, now);
|
|
71
|
-
return { keyboard: options.map(one => [
|
|
72
|
+
return { keyboard: options.map(one => [telegramButton(one.label, one.token)]), tokens: options.map(one => one.token) };
|
|
72
73
|
}
|
|
73
74
|
/** A tapped option, applied inside the update's transaction. */
|
|
74
75
|
export function applyFlowChoiceTap(store, binding, choice, message, repos, now) {
|
package/dist/telegram-mate.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type DoorOptions } from "./mate-doors.js";
|
|
|
4
4
|
import { type MateAsk, type Store, type TelegramBinding } from "./store.js";
|
|
5
5
|
import type { SubscriptionMateRunner } from "./subscription-chat.js";
|
|
6
6
|
import type { TelegramTransport } from "./telegram.js";
|
|
7
|
+
import { type TelegramCallbackButton, type TelegramUpdate } from "./contracts/telegram-callback.js";
|
|
7
8
|
/** How long one claimed turn may go without a heartbeat before another poller may take it over. */
|
|
8
9
|
export declare const CONVERSATION_CLAIM_MS: number;
|
|
9
10
|
/** A busy engine (one turn at a time) defers a queued message this long, up to the age bound below. */
|
|
@@ -74,19 +75,13 @@ export declare function phoneLinkButton(origin: string | null, link: PhoneLink |
|
|
|
74
75
|
/** The buttons under the lead's question: options two to a row, then "Something else". Each names the turn and the
|
|
75
76
|
* option's place, nothing more; a tap re-proves that the question is still open and is the tapper's own. */
|
|
76
77
|
export declare function askKeyboard(ask: MateAsk): CallbackKeyboard;
|
|
77
|
-
export type InlineButton = {
|
|
78
|
-
text: string;
|
|
79
|
-
callback_data: string;
|
|
80
|
-
} | {
|
|
78
|
+
export type InlineButton = TelegramCallbackButton | {
|
|
81
79
|
text: string;
|
|
82
80
|
url: string;
|
|
83
81
|
};
|
|
84
82
|
type Keyboard = InlineButton[][];
|
|
85
83
|
/** What a part persists: callback tokens only. A url is minted at send time, never stored. */
|
|
86
|
-
type CallbackKeyboard =
|
|
87
|
-
text: string;
|
|
88
|
-
callback_data: string;
|
|
89
|
-
}[][];
|
|
84
|
+
type CallbackKeyboard = TelegramCallbackButton[][];
|
|
90
85
|
/** Mint the card's tokens before the send, so a tap can never name a token that does not exist. */
|
|
91
86
|
export declare function mintCardTokens(store: Store, binding: TelegramBinding, proposal: number, now: Date, messageId?: string, chatId?: string): {
|
|
92
87
|
keyboard: CallbackKeyboard;
|
|
@@ -111,12 +106,7 @@ export type CardEffect = {
|
|
|
111
106
|
* ceiling; the door re-proves standing, ceiling, session and the
|
|
112
107
|
* proposal's own terms again inside its transaction.
|
|
113
108
|
*/
|
|
114
|
-
export declare function applyProposalTap(store: Store, binding: TelegramBinding, token: string, message: {
|
|
115
|
-
message_id: number;
|
|
116
|
-
chat?: {
|
|
117
|
-
id: number;
|
|
118
|
-
};
|
|
119
|
-
}, repos: readonly string[] | null, options: TelegramConversationOptions, now: Date): {
|
|
109
|
+
export declare function applyProposalTap(store: Store, binding: TelegramBinding, token: string, message: NonNullable<NonNullable<TelegramUpdate["callback_query"]>["message"]>, repos: readonly string[] | null, options: TelegramConversationOptions, now: Date): {
|
|
120
110
|
effects: CardEffect[];
|
|
121
111
|
confirmed: boolean;
|
|
122
112
|
ignored: boolean;
|
package/dist/telegram-mate.js
CHANGED
|
@@ -33,6 +33,7 @@ import { WARM_EMOJI, warmTurn } from "./chat-warmth.js";
|
|
|
33
33
|
import { TeamLeads } from "./team-leads.js";
|
|
34
34
|
import { chatResultHref } from "./chat-controls.js";
|
|
35
35
|
import { resultImageFileName, safeResultImageCaption, verifyResultImage } from "./chat-evidence.js";
|
|
36
|
+
import { telegramButton } from "./contracts/telegram-callback.js";
|
|
36
37
|
/** How long one claimed turn may go without a heartbeat before another poller may take it over. */
|
|
37
38
|
export const CONVERSATION_CLAIM_MS = 2 * 60_000;
|
|
38
39
|
/** A busy engine (one turn at a time) defers a queued message this long, up to the age bound below. */
|
|
@@ -110,8 +111,8 @@ function keyboardWith(callbacks, link) {
|
|
|
110
111
|
export function askKeyboard(ask) {
|
|
111
112
|
const rows = [];
|
|
112
113
|
for (let at = 0; at < ask.options.length; at += 2)
|
|
113
|
-
rows.push(ask.options.slice(at, at + 2).map((label, offset) => (
|
|
114
|
-
rows.push([
|
|
114
|
+
rows.push(ask.options.slice(at, at + 2).map((label, offset) => telegramButton(label.slice(0, 60), `ask:${ask.turn}:${at + offset}`)));
|
|
115
|
+
rows.push([telegramButton(MATE_ASK_OTHER, `ask:${ask.turn}:x`)]);
|
|
115
116
|
return rows;
|
|
116
117
|
}
|
|
117
118
|
/** Mint the card's tokens before the send, so a tap can never name a token that does not exist. */
|
|
@@ -121,7 +122,7 @@ export function mintCardTokens(store, binding, proposal, now, messageId, chatId
|
|
|
121
122
|
for (const [token, phase] of [[confirm, "confirm"], [dismiss, "dismiss"]]) {
|
|
122
123
|
store.createTelegramProposalAction({ token, binding: binding.id, proposal, phase, chatId, ttlMs: CARD_TTL_MS, ...(messageId === undefined ? {} : { messageId }) }, now);
|
|
123
124
|
}
|
|
124
|
-
return { keyboard: [[
|
|
125
|
+
return { keyboard: [[telegramButton("Confirm", confirm), telegramButton("Dismiss", dismiss)]], tokens: [confirm, dismiss] };
|
|
125
126
|
}
|
|
126
127
|
/**
|
|
127
128
|
* One tap on a proposal card, applied inside the update's own transaction
|
|
@@ -249,15 +250,15 @@ export function applyProposalTap(store, binding, token, message, repos, options,
|
|
|
249
250
|
if (challenged) {
|
|
250
251
|
ack("confirm it");
|
|
251
252
|
edit(`${body}\n\nThis records that you handled this exact result. Confirm?`, [
|
|
252
|
-
[
|
|
253
|
-
[
|
|
253
|
+
[telegramButton(`✓ Yes, ${CHAT_ACTIONS[sharedAction.operation].label.toLowerCase()}`, yes)],
|
|
254
|
+
[telegramButton("Cancel", cancel)],
|
|
254
255
|
]);
|
|
255
256
|
return;
|
|
256
257
|
}
|
|
257
258
|
ack("irreversible — confirm it");
|
|
258
259
|
edit(`⚠ This answer is IRREVERSIBLE.\n\n${body}\n\nConfirm?`, [
|
|
259
|
-
[
|
|
260
|
-
[
|
|
260
|
+
[telegramButton("⚠ Yes, answer it", yes)],
|
|
261
|
+
[telegramButton("Cancel", cancel)],
|
|
261
262
|
]);
|
|
262
263
|
};
|
|
263
264
|
if (action.phase === "confirm" && (irreversible || challenged)) {
|
package/dist/telegram-team.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { Store } from "./store.js";
|
|
10
10
|
import { type InlineButton } from "./telegram-mate.js";
|
|
11
11
|
import type { TelegramTransport } from "./telegram.js";
|
|
12
|
+
import type { TelegramMessage } from "./contracts/telegram-callback.js";
|
|
12
13
|
export type TeamCommand = {
|
|
13
14
|
kind: "list";
|
|
14
15
|
} | {
|
|
@@ -36,25 +37,7 @@ export type TeamInbound = {
|
|
|
36
37
|
projects: readonly string[] | null;
|
|
37
38
|
phoneOrigin: (() => string | null) | undefined;
|
|
38
39
|
updateId: number;
|
|
39
|
-
message:
|
|
40
|
-
message_id: number;
|
|
41
|
-
text?: string;
|
|
42
|
-
chat?: {
|
|
43
|
-
id: number;
|
|
44
|
-
type?: string;
|
|
45
|
-
};
|
|
46
|
-
from?: {
|
|
47
|
-
id: number;
|
|
48
|
-
};
|
|
49
|
-
reply_to_message?: {
|
|
50
|
-
message_id: number;
|
|
51
|
-
};
|
|
52
|
-
forward_origin?: unknown;
|
|
53
|
-
forward_date?: unknown;
|
|
54
|
-
via_bot?: unknown;
|
|
55
|
-
sender_chat?: unknown;
|
|
56
|
-
caption?: string;
|
|
57
|
-
};
|
|
40
|
+
message: TelegramMessage;
|
|
58
41
|
/** Queue one reply for after the update's transaction commits. */
|
|
59
42
|
say: (chatId: string, text: string, keyboard?: InlineButton[][]) => void;
|
|
60
43
|
};
|
package/dist/telegram.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export declare function telegramHookSecret(dir: string, make?: boolean): string
|
|
|
37
37
|
export declare function telegramPushUrl(hooksBase: string | null): string | null;
|
|
38
38
|
/** A push is Telegram's when its secret header matches ours (compared in constant time). */
|
|
39
39
|
export declare function pushedByTelegram(header: unknown, secret: string | null): boolean;
|
|
40
|
-
/** Keep one pushed update for the bridge
|
|
40
|
+
/** Keep one pushed update for the bridge. A schema-invalid JSON update is logged and acknowledged, never retried. */
|
|
41
41
|
export declare function keepPushedUpdate(store: Store, botId: string, body: Buffer, now: Date): {
|
|
42
42
|
ok: true;
|
|
43
43
|
kept: boolean;
|
package/dist/telegram.js
CHANGED
|
@@ -46,6 +46,7 @@ import { phoneCommand, phoneStatus, phoneTaskView, PHONE_CONSOLE_FOOTER, PHONE_H
|
|
|
46
46
|
import { MATE_MESSAGE_MAX_CHARS } from "./mate.js";
|
|
47
47
|
import { envValue } from "./names.js";
|
|
48
48
|
import { applyProposalTap, processTelegramConversations, phoneLinkButton, replyContextFor, telegramConversationRepos, telegramRequestId, tooLongText, whichTaskText, } from "./telegram-mate.js";
|
|
49
|
+
import { askOf, parseTelegramUpdate, pickOf, readTelegramButtonData, readTelegramUpdate, telegramButton } from "./contracts/telegram-callback.js";
|
|
49
50
|
/** The environment name — and therefore the name the builder strips from agents (both names; see names.ts). */
|
|
50
51
|
export { TELEGRAM_TOKEN_ENV as TOKEN_ENV, TELEGRAM_TOKEN_ENVS as TOKEN_ENVS } from "./names.js";
|
|
51
52
|
/** BotFather's shape: numeric bot id, colon, secret. */
|
|
@@ -99,19 +100,21 @@ export function pushedByTelegram(header, secret) {
|
|
|
99
100
|
const given = Buffer.from(header), wanted = Buffer.from(secret);
|
|
100
101
|
return given.length === wanted.length && timingSafeEqual(given, wanted);
|
|
101
102
|
}
|
|
102
|
-
/** Keep one pushed update for the bridge
|
|
103
|
+
/** Keep one pushed update for the bridge. A schema-invalid JSON update is logged and acknowledged, never retried. */
|
|
103
104
|
export function keepPushedUpdate(store, botId, body, now) {
|
|
104
|
-
let
|
|
105
|
+
let raw;
|
|
105
106
|
try {
|
|
106
|
-
|
|
107
|
+
raw = JSON.parse(body.toString("utf8"));
|
|
107
108
|
}
|
|
108
109
|
catch {
|
|
109
110
|
return { ok: false };
|
|
110
111
|
}
|
|
111
|
-
const
|
|
112
|
-
if (update
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
const update = readTelegramUpdate(raw);
|
|
113
|
+
if (!update.ok) {
|
|
114
|
+
console.warn(`Ignoring a pushed Telegram update: ${update.issues.map(issue => issue.line).join("; ")}`);
|
|
115
|
+
return { ok: true, kept: false };
|
|
116
|
+
}
|
|
117
|
+
return { ok: true, kept: store.queueTelegramUpdate(botId, update.value.update_id, JSON.stringify(raw), now) };
|
|
115
118
|
}
|
|
116
119
|
/**
|
|
117
120
|
* Environment wins, the credential file beside the database otherwise.
|
|
@@ -623,7 +626,7 @@ function decideKeyboard(phoneOrigin, rows) {
|
|
|
623
626
|
catch {
|
|
624
627
|
origin = null;
|
|
625
628
|
}
|
|
626
|
-
return rows.map(row => row.flatMap((one) => "token" in one ? [
|
|
629
|
+
return rows.map(row => row.flatMap((one) => "token" in one ? [telegramButton(one.label, one.token)]
|
|
627
630
|
: phoneLinkButton(origin, one.link) ?? [])).filter(row => row.length > 0);
|
|
628
631
|
}
|
|
629
632
|
/** What this card may act on in the chat now, for this person under their current ceiling; null keeps its link. */
|
|
@@ -1311,10 +1314,7 @@ async function deliverOne(store, botId, binding, sender, notification, clock, ph
|
|
|
1311
1314
|
}, clock());
|
|
1312
1315
|
}
|
|
1313
1316
|
const keyboard = tokens.map(({ option, token }) => [
|
|
1314
|
-
{
|
|
1315
|
-
text: `${option.label}${option.id === decision.recommendation ? " ✓" : ""}${option.reversible ? "" : " ⚠"}`,
|
|
1316
|
-
callback_data: token,
|
|
1317
|
-
},
|
|
1317
|
+
telegramButton(`${option.label}${option.id === decision.recommendation ? " ✓" : ""}${option.reversible ? "" : " ⚠"}`, token),
|
|
1318
1318
|
]);
|
|
1319
1319
|
const last = parts[parts.length - 1];
|
|
1320
1320
|
const sent = await sender(last, keyboard);
|
|
@@ -1433,12 +1433,22 @@ async function drainUpdates(store, botId, transport, owner, generation, cursor,
|
|
|
1433
1433
|
report.problems.push(`getUpdates: ${said}`);
|
|
1434
1434
|
return;
|
|
1435
1435
|
}
|
|
1436
|
-
const updates = answer.result
|
|
1436
|
+
const updates = Array.isArray(answer.result) ? answer.result : [];
|
|
1437
1437
|
if (updates.length === 0)
|
|
1438
1438
|
return;
|
|
1439
|
-
for (const
|
|
1440
|
-
|
|
1441
|
-
|
|
1439
|
+
for (const raw of updates) {
|
|
1440
|
+
const update = readTelegramUpdate(raw);
|
|
1441
|
+
if (!update.ok) {
|
|
1442
|
+
// Telegram's own update in a shape this bridge doesn't read: said, and passed over so the next one is read.
|
|
1443
|
+
const id = raw?.update_id;
|
|
1444
|
+
report.problems.push(`an update Telegram sent couldn't be read: ${update.issues.map(issue => issue.line).join("; ")}`);
|
|
1445
|
+
if (typeof id !== "number" || !Number.isSafeInteger(id))
|
|
1446
|
+
return;
|
|
1447
|
+
offset = id + 1;
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
await processUpdate(context, update.value, owner, generation);
|
|
1451
|
+
offset = update.value.update_id + 1;
|
|
1442
1452
|
}
|
|
1443
1453
|
}
|
|
1444
1454
|
// The budget ran out with Telegram still holding pages: said, not hidden.
|
|
@@ -1473,15 +1483,11 @@ async function processUpdate(context, update, owner, generation) {
|
|
|
1473
1483
|
async function drainInbox(context, owner, generation) {
|
|
1474
1484
|
let applied = 0;
|
|
1475
1485
|
for (const queued of context.store.telegramInbox(context.botId, PAGE_BUDGET * 100)) {
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
update
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
update = null;
|
|
1482
|
-
}
|
|
1483
|
-
if (update !== null && update.update_id === queued.updateId)
|
|
1484
|
-
await processUpdate(context, update, owner, generation);
|
|
1486
|
+
const update = parseTelegramUpdate(queued.payload);
|
|
1487
|
+
if (update.ok && update.value.update_id === queued.updateId)
|
|
1488
|
+
await processUpdate(context, update.value, owner, generation);
|
|
1489
|
+
else if (!update.ok)
|
|
1490
|
+
context.report.problems.push(`a pushed update couldn't be read: ${update.issues.map(issue => issue.line).join("; ")}`);
|
|
1485
1491
|
context.store.dropTelegramInbox(queued.updateId);
|
|
1486
1492
|
applied++;
|
|
1487
1493
|
}
|
|
@@ -1843,10 +1849,10 @@ function pickKeyboard(store, choices, focused) {
|
|
|
1843
1849
|
for (const one of choices) {
|
|
1844
1850
|
const ref = store.lookupRef(one.id);
|
|
1845
1851
|
if (ref !== null)
|
|
1846
|
-
rows.push([
|
|
1852
|
+
rows.push([telegramButton(`${one.title} · ${one.label}`.slice(0, 60), `pick:${ref.id}`)]);
|
|
1847
1853
|
}
|
|
1848
1854
|
if (focused)
|
|
1849
|
-
rows.push([
|
|
1855
|
+
rows.push([telegramButton("Back to the lead", "pick:lead")]);
|
|
1850
1856
|
return rows;
|
|
1851
1857
|
}
|
|
1852
1858
|
/** The title of the task this Telegram chat chose to talk about, if any. */
|
|
@@ -2106,23 +2112,30 @@ function applyCallback(context, update, effects) {
|
|
|
2106
2112
|
report.ignored++;
|
|
2107
2113
|
return;
|
|
2108
2114
|
}
|
|
2115
|
+
// The paired person, in their chat: a button whose data isn't one Toolroll makes is answered with why, and does nothing.
|
|
2116
|
+
const data = readTelegramButtonData(callback.data);
|
|
2117
|
+
if (!data.ok) {
|
|
2118
|
+
report.ignored++;
|
|
2119
|
+
ack(`That button couldn't be read (${data.issues.map(issue => issue.line).join("; ")}). Nothing was done.`.slice(0, 190));
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2109
2122
|
// A task picked from /tasks: this private chat now talks about it (or,
|
|
2110
2123
|
// "Back to the lead", about everything again). The task is re-proved
|
|
2111
2124
|
// against the chat's ceiling at the tap.
|
|
2112
|
-
|
|
2125
|
+
const pick = pickOf(token);
|
|
2126
|
+
if (pick !== null) {
|
|
2113
2127
|
if (tapChat !== binding.chatId) {
|
|
2114
2128
|
report.ignored++;
|
|
2115
2129
|
return;
|
|
2116
2130
|
}
|
|
2117
|
-
if (
|
|
2131
|
+
if (pick.ref === null) {
|
|
2118
2132
|
store.setChatFocus("telegram", binding.id, null, clock());
|
|
2119
2133
|
store.recordTelegramTaskMessage(binding, String(message.message_id), null, null, clock());
|
|
2120
2134
|
ack("Back to the lead");
|
|
2121
2135
|
editText(PHONE_BACK_TO_LEAD);
|
|
2122
2136
|
return;
|
|
2123
2137
|
}
|
|
2124
|
-
const
|
|
2125
|
-
const taskId = refId === undefined ? null : store.externalIdFor(Number(refId));
|
|
2138
|
+
const taskId = store.externalIdFor(pick.ref);
|
|
2126
2139
|
const repos = context.projects === null ? null : telegramConversationRepos(store, binding.approver, context.projects);
|
|
2127
2140
|
if (taskId === null || repos === null || !taskInCeiling(store, taskId, repos)) {
|
|
2128
2141
|
ack("That task isn't available here now.");
|
|
@@ -2136,7 +2149,7 @@ function applyCallback(context, update, effects) {
|
|
|
2136
2149
|
const current = store.taskFamilyOf(taskId, repos, false)?.current.id ?? id;
|
|
2137
2150
|
const view = phoneTaskView(store, repos, current, clock());
|
|
2138
2151
|
store.recordTelegramTaskMessage(binding, String(message.message_id), current, view.run, clock());
|
|
2139
|
-
editText(phoneFocusText(view.text), [[
|
|
2152
|
+
editText(phoneFocusText(view.text), [[telegramButton("Back to the lead", "pick:lead")]]);
|
|
2140
2153
|
return;
|
|
2141
2154
|
}
|
|
2142
2155
|
// A teammate's question (v93): an option answers it; "Answer in words" asks for a reply.
|
|
@@ -2206,13 +2219,13 @@ function applyCallback(context, update, effects) {
|
|
|
2206
2219
|
return;
|
|
2207
2220
|
}
|
|
2208
2221
|
// The lead's question to its owner: an option is sent as their next message; "Something else" asks them to type it.
|
|
2209
|
-
const askTap =
|
|
2222
|
+
const askTap = askOf(token);
|
|
2210
2223
|
if (askTap !== null) {
|
|
2211
2224
|
if (tapChat !== binding.chatId || context.conversation === undefined) {
|
|
2212
2225
|
report.ignored++;
|
|
2213
2226
|
return;
|
|
2214
2227
|
}
|
|
2215
|
-
const turn =
|
|
2228
|
+
const turn = askTap.turn;
|
|
2216
2229
|
const found = store.getMateTurn(turn)?.approver === binding.approver ? store.mateAskState(turn, clock()) : { state: "expired" };
|
|
2217
2230
|
if (found.state !== "open") {
|
|
2218
2231
|
const line = found.state === "answered" ? "That question was already answered." : "That question expired. Send your answer as a message.";
|
|
@@ -2225,11 +2238,11 @@ function applyCallback(context, update, effects) {
|
|
|
2225
2238
|
ack("Your answer is on its way.");
|
|
2226
2239
|
return;
|
|
2227
2240
|
}
|
|
2228
|
-
if (askTap
|
|
2241
|
+
if (askTap.option === "x") {
|
|
2229
2242
|
ack("Type your answer as a message.");
|
|
2230
2243
|
return;
|
|
2231
2244
|
}
|
|
2232
|
-
const option = ask.options[
|
|
2245
|
+
const option = ask.options[askTap.option];
|
|
2233
2246
|
if (option === undefined) {
|
|
2234
2247
|
ack("That option is no longer there.");
|
|
2235
2248
|
return;
|
|
@@ -2370,8 +2383,8 @@ function applyCallback(context, update, effects) {
|
|
|
2370
2383
|
store.createTelegramAction({ token: cancel, binding: binding.id, decision: decision.id, optionId: option.id, phase: "cancel", chatId: binding.chatId, messageId: placedOn, ttlMs: CONFIRM_TTL_MS }, clock());
|
|
2371
2384
|
ack("irreversible — confirm it");
|
|
2372
2385
|
editText(`⚠ ${option.label} is IRREVERSIBLE.\n${option.consequence}\n${draft === null ? "" : `\nWith your note:\n${draft.note.split("\n").map(line => `| ${line}`).join("\n")}\n`}\nConfirm?`, [
|
|
2373
|
-
[
|
|
2374
|
-
[
|
|
2386
|
+
[telegramButton(`⚠ Yes, ${option.label}`, confirm)],
|
|
2387
|
+
[telegramButton("Cancel", cancel)],
|
|
2375
2388
|
]);
|
|
2376
2389
|
return;
|
|
2377
2390
|
}
|
|
@@ -2418,10 +2431,7 @@ function applyCallback(context, update, effects) {
|
|
|
2418
2431
|
}
|
|
2419
2432
|
ack("cancelled");
|
|
2420
2433
|
editText(`Q: ${decision.question}`, fresh.map(({ option, token: choose }) => [
|
|
2421
|
-
{
|
|
2422
|
-
text: `${option.label}${option.id === decision.recommendation ? " ✓" : ""}${option.reversible ? "" : " ⚠"}`,
|
|
2423
|
-
callback_data: choose,
|
|
2424
|
-
},
|
|
2434
|
+
telegramButton(`${option.label}${option.id === decision.recommendation ? " ✓" : ""}${option.reversible ? "" : " ⚠"}`, choose),
|
|
2425
2435
|
]));
|
|
2426
2436
|
}
|
|
2427
2437
|
function answerNow(context, decision, choice, binding, ack, editText) {
|