@segmentstream/agent 0.5.4 → 0.6.4

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.
Files changed (41) hide show
  1. package/README.md +78 -18
  2. package/dist/extension/connections/ontology.mjs +2 -2
  3. package/dist/extension/instructions.md +10 -9
  4. package/dist/extension/lib/attachment-tools.mjs +1 -3
  5. package/dist/extension/lib/chat-post-confirmation.d.ts +23 -0
  6. package/dist/extension/lib/chat-post-confirmation.mjs +61 -0
  7. package/dist/extension/lib/confirmation-description.d.ts +2 -0
  8. package/dist/extension/lib/confirmations.d.ts +5 -0
  9. package/dist/extension/lib/confirmations.mjs +25 -5
  10. package/dist/extension/lib/conversations.d.ts +12 -1
  11. package/dist/extension/lib/conversations.mjs +4 -3
  12. package/dist/extension/lib/ontology-connection.d.ts +4 -0
  13. package/dist/extension/lib/ontology-connection.mjs +46 -3
  14. package/dist/extension/lib/ontology-tools.d.ts +1 -0
  15. package/dist/extension/lib/ontology-tools.mjs +27 -2
  16. package/dist/extension/lib/telegram/adapter.d.ts +1 -1
  17. package/dist/extension/lib/telegram/adapter.mjs +18 -28
  18. package/dist/extension/lib/telegram/attachments-send.d.ts +7 -3
  19. package/dist/extension/lib/telegram/attachments-send.mjs +25 -7
  20. package/dist/extension/lib/telegram/chat-post-card.d.ts +15 -0
  21. package/dist/extension/lib/telegram/chat-post-card.mjs +65 -0
  22. package/dist/extension/lib/telegram/confirmations.d.ts +3 -2
  23. package/dist/extension/lib/telegram/confirmations.mjs +140 -21
  24. package/dist/extension/lib/telegram/conversations.mjs +5 -3
  25. package/dist/extension/lib/telegram/customer-text.d.ts +13 -1
  26. package/dist/extension/lib/telegram/customer-text.mjs +108 -7
  27. package/dist/extension/lib/telegram/format.d.ts +17 -0
  28. package/dist/extension/lib/telegram/format.mjs +171 -0
  29. package/dist/extension/lib/telegram/health.mjs +1 -1
  30. package/dist/extension/lib/telegram/inbound-request.d.ts +1 -1
  31. package/dist/extension/lib/telegram/proxy-fetch.d.ts +1 -1
  32. package/dist/extension/lib/telegram/proxy-fetch.mjs +14 -6
  33. package/dist/extension/lib/telegram/runtime-request.d.ts +8 -0
  34. package/dist/extension/lib/telegram/runtime-request.mjs +36 -0
  35. package/dist/extension/lib/telegram/send-queue.d.ts +4 -0
  36. package/dist/extension/lib/telegram/send-queue.mjs +35 -0
  37. package/dist/extension/lib/telegram/silent.d.ts +3 -3
  38. package/dist/extension/lib/telegram/silent.mjs +19 -4
  39. package/package.json +2 -1
  40. package/dist/extension/lib/telegram/requests.d.ts +0 -80
  41. package/dist/extension/lib/telegram/requests.mjs +0 -186
@@ -14,6 +14,7 @@ import { agentIdentities } from "../identities.mjs";
14
14
  import { conversations } from "../conversations.mjs";
15
15
  import { observedMessages } from "./observed.mjs";
16
16
  import { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText } from "./inbound-request.mjs";
17
+ import { telegramSendScope } from "./send-queue.mjs";
17
18
  import { failureText, reportTelegramError, safeTelegramFetch, withDeliveryLocale } from "./customer-text.mjs";
18
19
  import { captureRouteDecider, captureTelegramFetch, conversationIdentities, telegramConversationCapture } from "./conversations.mjs";
19
20
  import { runtimeTokenVerifier } from "../runtime-identity.mjs";
@@ -29,7 +30,6 @@ import { replyDelivery } from "./silent.mjs";
29
30
  import { downloadTelegramFile, gatewayTranscriber } from "./voice.mjs";
30
31
  import { handleInboundMessage } from "./inbound.mjs";
31
32
  import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
32
- import { messagesRoute } from "./requests.mjs";
33
33
  import { healthRoute } from "./health.mjs";
34
34
  import { telegramFailureEvents } from "./turn-errors.mjs";
35
35
  import { telegramWebhookVerifier } from "./webhook.mjs";
@@ -128,7 +128,7 @@ function segmentstreamTelegram(options) {
128
128
  api: {
129
129
  apiBaseUrl: TELEGRAM_PROXY_ORIGIN,
130
130
  fileBaseUrl: TELEGRAM_PROXY_ORIGIN,
131
- fetch: (input, init) => safeTelegramFetch(captureTelegramFetch(telegramProxyFetch(proxy()), archive, pairing))(input, init)
131
+ fetch: (input, init) => safeTelegramFetch(captureTelegramFetch(telegramProxyFetch(proxy(), telegramSendScope(environment, settings().connection)), archive, pairing), telegramSendScope(environment, settings().connection))(input, init)
132
132
  },
133
133
  credentials: {
134
134
  botToken: TELEGRAM_PLACEHOLDER_TOKEN,
@@ -228,8 +228,8 @@ function segmentstreamTelegram(options) {
228
228
  },
229
229
  events: {
230
230
  ...telegramFailureEvents,
231
- ...telegramConfirmationEvents(confirmationRenderer({ model: () => options.models().decision })),
232
- "message.completed": replyDelivery(observed)
231
+ ...telegramConfirmationEvents(confirmationRenderer({ model: () => options.models().decision }), archive),
232
+ "message.completed": replyDelivery(observed, () => telegramSendScope(environment, settings().connection))
233
233
  }
234
234
  });
235
235
  const adapter = channel.adapter;
@@ -263,28 +263,17 @@ function segmentstreamTelegram(options) {
263
263
  }, context.state.messageThreadId === null ? void 0 : String(context.state.messageThreadId));
264
264
  }]))
265
265
  } },
266
- routes: [
267
- ...channel.routes.map((route) => gatedRoute({
268
- route,
269
- options,
270
- environment,
271
- capture
272
- })),
273
- messagesRoute({
274
- channel,
275
- enabled: () => channelsEnabled(environment),
276
- configured: () => options.settings() !== null,
277
- verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment)),
278
- sessionPolicy: policy,
279
- scope: () => options.sessions().scope
280
- }),
281
- healthRoute({
282
- chats,
283
- enabled: () => channelsEnabled(environment),
284
- configured: () => options.settings() !== null,
285
- verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment))
286
- })
287
- ]
266
+ routes: [...channel.routes.map((route) => gatedRoute({
267
+ route,
268
+ options,
269
+ environment,
270
+ capture
271
+ })), healthRoute({
272
+ chats,
273
+ enabled: () => channelsEnabled(environment),
274
+ configured: () => options.settings() !== null,
275
+ verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment))
276
+ })]
288
277
  };
289
278
  }
290
279
  function sessionIdOf(context) {
@@ -305,9 +294,10 @@ function gatedRoute(input) {
305
294
  resolveSession: args.resolveSession,
306
295
  from: args.from
307
296
  };
308
- inbound.respond = (address, requestId, decision, auth) => args.from(scoped(address)).respond([{
297
+ inbound.respond = (address, requestId, decision, auth, text) => args.from(scoped(address)).respond([{
309
298
  requestId,
310
- optionId: decision
299
+ optionId: decision,
300
+ ...text === void 0 ? {} : { text }
311
301
  }], { auth });
312
302
  const tasks = [];
313
303
  const waitUntil = (task) => {
@@ -24,7 +24,7 @@ export interface SentFile {
24
24
  /** Bare ids are document records; explicit references select other entities and file properties. */
25
25
  export declare function sendAttachmentsInput(value: Record<string, unknown>): SendAttachmentsInput;
26
26
  /** All destinations and credentials come from the channel's turn and mounted configuration. */
27
- export declare function sendAttachments(options: {
27
+ interface SendOptions {
28
28
  readonly input: SendAttachmentsInput;
29
29
  readonly principal: SessionAuthContext | null;
30
30
  readonly sessionId: string;
@@ -32,10 +32,14 @@ export declare function sendAttachments(options: {
32
32
  readonly environment?: Environment;
33
33
  readonly token?: RuntimeToken;
34
34
  readonly fetch?: typeof fetch;
35
- }): Promise<{
35
+ }
36
+ type SendResult = {
36
37
  readonly files: ReadonlyArray<SentFile>;
37
38
  } | {
38
39
  readonly error: string;
39
40
  readonly detail: string;
40
41
  readonly reference: string;
41
- }>;
42
+ };
43
+ /** Reserve the chat before downloading, and keep every album/batch in the same queue slot. */
44
+ export declare function sendAttachments(options: SendOptions): Promise<SendResult>;
45
+ export {};
@@ -11,7 +11,8 @@ import { redactDiagnostic } from "../diagnostics.mjs";
11
11
  import { runtimeToken } from "../runtime-token.mjs";
12
12
  import { ACT_AS_HEADER } from "../ontology-connection.mjs";
13
13
  import { AttachmentError, CONVERSATION_HEADER, SESSION_HEADER } from "../attachments.mjs";
14
- import { customerCopy, customerText, reportTelegramError } from "./customer-text.mjs";
14
+ import { inTelegramSendQueue, telegramSendScope } from "./send-queue.mjs";
15
+ import { customerCopy, customerMessage, formattingRefused, reportTelegramError } from "./customer-text.mjs";
15
16
  import { confirmationLocale } from "./confirmation-text.mjs";
16
17
  import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
17
18
  import { attachmentOwner } from "../attachment-tools.mjs";
@@ -74,7 +75,11 @@ async function boundedDownload(response, maximum) {
74
75
  }
75
76
  return new Uint8Array(Buffer.concat(chunks));
76
77
  }
77
- async function sendAttachments(options) {
78
+ function sendAttachments(options) {
79
+ const chatId = options.principal?.attributes.chatId;
80
+ return typeof chatId === "string" && options.connection !== void 0 ? inTelegramSendQueue(telegramSendScope(options.environment ?? process.env, options.connection), chatId, () => sendAttachmentsInOrder(options)) : sendAttachmentsInOrder(options);
81
+ }
82
+ async function sendAttachmentsInOrder(options) {
78
83
  const environment = options.environment ?? process.env;
79
84
  const principal = options.principal;
80
85
  const locale = confirmationLocale(principal);
@@ -117,7 +122,7 @@ async function sendAttachments(options) {
117
122
  connection: options.connection,
118
123
  token,
119
124
  fetch: fetchImpl
120
- }));
125
+ }), telegramSendScope(environment, options.connection));
121
126
  const results = options.input.documentIds.map(({ id }) => ({
122
127
  id,
123
128
  filename: ""
@@ -196,12 +201,18 @@ async function sendAttachments(options) {
196
201
  if (files.length === 1) {
197
202
  const file = files[0];
198
203
  form.set(file.type, file.blob, file.filename);
199
- if (caption !== void 0) form.set("caption", caption);
204
+ if (caption !== void 0) {
205
+ form.set("caption", caption.text);
206
+ if (caption.entities.length > 0) form.set("caption_entities", JSON.stringify(caption.entities));
207
+ }
200
208
  } else {
201
209
  form.set("media", JSON.stringify(files.map((file, index) => ({
202
210
  type: file.type,
203
211
  media: `attach://file${index}`,
204
- ...index === 0 && caption !== void 0 ? { caption } : {}
212
+ ...index === 0 && caption !== void 0 ? {
213
+ caption: caption.text,
214
+ ...caption.entities.length > 0 ? { caption_entities: caption.entities } : {}
215
+ } : {}
205
216
  }))));
206
217
  files.forEach((file, index) => form.set(`file${index}`, file.blob, file.filename));
207
218
  }
@@ -211,7 +222,7 @@ async function sendAttachments(options) {
211
222
  body: form
212
223
  });
213
224
  }
214
- let caption = options.input.caption === void 0 ? void 0 : customerText(options.input.caption, locale);
225
+ let caption = options.input.caption === void 0 ? void 0 : customerMessage(options.input.caption, locale);
215
226
  let cursor = 0;
216
227
  while (cursor < uploads.length) {
217
228
  const batch = [uploads[cursor++]];
@@ -227,7 +238,14 @@ async function sendAttachments(options) {
227
238
  }
228
239
  try {
229
240
  if (size > 2097152) throw refused("too_large", PROXY_LIMIT);
230
- const response = await telegram(request);
241
+ let response = await telegram(request);
242
+ if (caption !== void 0 && caption.entities.length > 0 && await formattingRefused(response)) {
243
+ caption = {
244
+ text: caption.text,
245
+ entities: []
246
+ };
247
+ response = await telegram(multipart(batch, caption));
248
+ }
231
249
  const answer = await response.json();
232
250
  if (!response.ok || !isRecord(answer) || answer.ok !== true) {
233
251
  console.error("[segmentstream] Telegram attachment upload refused", {
@@ -0,0 +1,15 @@
1
+ import type { ChatPostPreview } from "../chat-post-confirmation.js";
2
+ import type { ConfirmationText } from "./confirmation-text.js";
3
+ /** Only the frozen deterministic card may bypass the generic technical-prose filter. */
4
+ export declare function withChatPostCard<T>(text: string, run: () => T): T;
5
+ export declare function isExactChatPostCard(text: string): boolean;
6
+ export declare function chatPostCopy(locale: string): {
7
+ edit: string;
8
+ changes: string;
9
+ uncertain: string;
10
+ sent: string;
11
+ history: string;
12
+ expired: string;
13
+ };
14
+ export declare function chatPostCard(preview: ChatPostPreview, locale: string): ConfirmationText;
15
+ export declare function chatPostReceipt(output: unknown, locale: string): string | undefined;
@@ -0,0 +1,65 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import { AsyncLocalStorage } from "node:async_hooks";
8
+ const MAX_PREVIEW_LENGTH = 3584;
9
+ const exactText = new AsyncLocalStorage();
10
+ function withChatPostCard(text, run) {
11
+ if (text.length > 4096) throw new Error("The complete chat confirmation cannot fit in one Telegram message.");
12
+ return exactText.run(text, run);
13
+ }
14
+ function isExactChatPostCard(text) {
15
+ return exactText.getStore() === text;
16
+ }
17
+ function chatPostCopy(locale) {
18
+ return locale.startsWith("ru") ? {
19
+ edit: "Изменить",
20
+ changes: "Что изменить в сообщении?",
21
+ uncertain: "Неизвестно, отправлено ли сообщение. Не отправляйте его повторно автоматически.",
22
+ sent: "Отправлено.",
23
+ history: "Не удалось записать квитанцию в историю.",
24
+ expired: "Срок подтверждения истёк. Ничего не отправлено."
25
+ } : {
26
+ edit: "Edit",
27
+ changes: "What would you like to change in the message?",
28
+ uncertain: "Delivery is uncertain. Do not resend automatically.",
29
+ sent: "Sent.",
30
+ history: "The receipt could not be recorded in history.",
31
+ expired: "This confirmation expired. Nothing was sent."
32
+ };
33
+ }
34
+ function chatPostCard(preview, locale) {
35
+ const russian = locale.startsWith("ru");
36
+ const text = [
37
+ russian ? "Отправить сообщение?" : "Send message?",
38
+ `${russian ? "Чат" : "Chat"}: ${preview.title} (${preview.provider})`,
39
+ ...preview.replyTo === null ? [] : [`${russian ? "В ответ на" : "Reply to"}: ${preview.replyTo}`],
40
+ "",
41
+ preview.finalText
42
+ ].join("\n");
43
+ if (text.length > MAX_PREVIEW_LENGTH) throw new Error("The full message and receipt cannot fit in one Telegram confirmation. Make a shorter draft.");
44
+ return {
45
+ text,
46
+ confirm: russian ? "Отправить" : "Confirm",
47
+ cancel: russian ? "Отмена" : "Cancel",
48
+ executing: russian ? "Подтверждено." : "Confirmed.",
49
+ cancelled: russian ? "Отменено." : "Cancelled.",
50
+ completed: chatPostCopy(locale).sent,
51
+ failed: russian ? "Ошибка отправки сообщения." : "Chat posting failed."
52
+ };
53
+ }
54
+ function chatPostReceipt(output, locale) {
55
+ const value = typeof output === "object" && output !== null ? output : {};
56
+ if (value.status !== "sent" && value.status !== "uncertain" || typeof value.history_recorded !== "boolean" || typeof value.duplicate !== "boolean" || value.message_id !== null && typeof value.message_id !== "string" || value.url !== null && typeof value.url !== "string") return;
57
+ const copy = chatPostCopy(locale);
58
+ const url = typeof value.url === "string" && value.url.length <= 256 && /^https:\/\/t\.me\/[A-Za-z0-9_/-]+$/u.test(value.url) ? value.url : null;
59
+ return [
60
+ value.status === "sent" ? copy.sent : copy.uncertain,
61
+ ...url === null ? [] : [url],
62
+ ...value.history_recorded ? [] : [copy.history]
63
+ ].join("\n");
64
+ }
65
+ export { chatPostCard, chatPostCopy, chatPostReceipt, isExactChatPostCard, withChatPostCard };
@@ -1,10 +1,11 @@
1
1
  import type { ChannelEvents } from "eve/channels";
2
2
  import type { TelegramCallbackQuery, TelegramChannelEvents, TelegramContext, TelegramEventContext } from "eve/channels/telegram";
3
+ import { type Conversations } from "../conversations.js";
3
4
  import type { AgentIdentities } from "../identities.js";
4
5
  import { type PendingQuestion } from "../questions.js";
5
6
  import { type ConfirmationRenderer } from "./confirmation-text.js";
6
7
  import { type HistoryReader, type RecordedEvent } from "./sessions.js";
7
- export declare function confirmationCallback(requestId: string, memberId: string, decision: "approve" | "cancel"): string;
8
+ export declare function confirmationCallback(requestId: string, memberId: string, decision: "approve" | "cancel" | "edit"): string;
8
9
  export declare function questionFromHistory(events: readonly RecordedEvent[], owner?: Pick<PendingQuestion, "memberId" | "chatId" | "messageThreadId">): PendingQuestion | undefined;
9
10
  export declare function questionAddress(owner: Pick<PendingQuestion, "memberId" | "chatId" | "messageThreadId">): string;
10
11
  /** Custom callback data avoids Eve's default, unpaired Telegram callback principal. */
@@ -16,5 +17,5 @@ export declare function handleConfirmationCallback(input: {
16
17
  readonly scope: "chat" | "thread";
17
18
  readonly now?: number;
18
19
  }): Promise<void>;
19
- export declare function telegramConfirmationEvents(render?: ConfirmationRenderer): TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.cleared">;
20
+ export declare function telegramConfirmationEvents(render?: ConfirmationRenderer, archive?: Conversations): TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.cleared">;
20
21
  export declare const confirmationEvents: TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.cleared">;
@@ -5,7 +5,10 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { conversationLanguage, memberIdOf, telegramPrincipal } from "./principal.mjs";
8
+ import { chatPostOperationId, isChatPost } from "../chat-post-confirmation.mjs";
9
+ import { chatPostCard, chatPostCopy, chatPostReceipt, withChatPostCard } from "./chat-post-card.mjs";
8
10
  import { confirmations } from "../confirmations.mjs";
11
+ import { conversationEvent } from "../conversations.mjs";
9
12
  import { currentInboundRequest } from "./inbound-request.mjs";
10
13
  import { currentLocale, failureText, reportTelegramError } from "./customer-text.mjs";
11
14
  import { answerMarker, isPendingQuestion, pendingQuestions, questionKey } from "../questions.mjs";
@@ -18,7 +21,8 @@ function shortHash(value) {
18
21
  return createHash("sha256").update(value).digest("hex").slice(0, 20);
19
22
  }
20
23
  function confirmationCallback(requestId, memberId, decision) {
21
- return `${PREFIX}${shortHash(requestId)}:${shortHash(memberId)}:${decision === "approve" ? "y" : "n"}`;
24
+ const choice = decision === "approve" ? "y" : decision === "edit" ? "e" : "n";
25
+ return `${PREFIX}${shortHash(requestId)}:${shortHash(memberId)}:${choice}`;
22
26
  }
23
27
  function object(value) {
24
28
  return typeof value === "object" && value !== null ? value : {};
@@ -34,7 +38,9 @@ function openConfirmations(events) {
34
38
  if (request.kind === "tool-approval" && typeof request.requestId === "string" && typeof action.callId === "string") open.set(request.requestId, {
35
39
  requestId: request.requestId,
36
40
  callId: action.callId,
37
- at: Date.parse(event.meta.at)
41
+ at: Date.parse(event.meta.at),
42
+ toolName: String(action.toolName ?? ""),
43
+ input: object(action.input)
38
44
  });
39
45
  }
40
46
  if (event.type === "input.resolved" && Array.isArray(data.resolutions)) for (const item of data.resolutions) {
@@ -49,6 +55,10 @@ function questionFromHistory(events, owner) {
49
55
  for (const event of events) {
50
56
  const data = object(event.data);
51
57
  if (event.type === "context.cleared") pending.clear();
58
+ if (event.type === "input.resolved" && Array.isArray(data.resolutions)) for (const resolution of data.resolutions) {
59
+ const question = chatPostEditQuestion(object(resolution));
60
+ if (question !== void 0) pending.set(questionKey(question), question);
61
+ }
52
62
  if (event.type === "action.result") {
53
63
  const result = object(data.result);
54
64
  const candidate = object(result.output).pendingQuestion;
@@ -60,6 +70,16 @@ function questionFromHistory(events, owner) {
60
70
  }
61
71
  return owner === void 0 ? [...pending.values()].at(-1) : pending.get(questionKey(owner));
62
72
  }
73
+ function chatPostEditQuestion(resolution) {
74
+ const text = object(resolution.response).text;
75
+ if (resolution.outcome !== "cancelled" || typeof text !== "string") return;
76
+ try {
77
+ const question = object(JSON.parse(text)).pendingChatPostEdit;
78
+ return isPendingQuestion(question) ? question : void 0;
79
+ } catch {
80
+ return;
81
+ }
82
+ }
63
83
  function questionAddress(owner) {
64
84
  return formatAddress({
65
85
  ...owner,
@@ -111,12 +131,17 @@ async function handleConfirmationCallback(input) {
111
131
  }), input.scope);
112
132
  const session = await (currentInboundRequest()?.sessions)?.resolveSession(address);
113
133
  const prompt = session === void 0 ? void 0 : openConfirmations(await input.read(session.id)).find((candidate) => shortHash(candidate.requestId) === requestHash);
114
- if (prompt === void 0 || choice !== "y" && choice !== "n") {
134
+ if (prompt === void 0 || ![
135
+ "y",
136
+ "n",
137
+ "e"
138
+ ].includes(choice ?? "") || choice === "e" && !isChatPost(prompt.toolName)) {
115
139
  await notice("This confirmation is no longer pending.");
116
140
  return;
117
141
  }
118
142
  const expired = !Number.isFinite(prompt.at) || prompt.at + 6e5 <= (input.now ?? Date.now());
119
- const decision = expired || choice === "n" ? "cancel" : "approve";
143
+ const decision = expired || choice !== "y" ? "cancel" : "approve";
144
+ const edit = !expired && choice === "e";
120
145
  const principal = telegramPrincipal({
121
146
  member: lookup.member,
122
147
  message: {
@@ -130,17 +155,58 @@ async function handleConfirmationCallback(input) {
130
155
  });
131
156
  const source = currentInboundRequest()?.respond;
132
157
  if (source === void 0) throw new Error("Confirmation callback has no session continuation.");
158
+ const editQuestion = edit ? {
159
+ id: `edit-${prompt.callId}`,
160
+ memberId: lookup.member.id,
161
+ chatId: query.message.chat.id,
162
+ messageThreadId: String(query.message.messageThreadId ?? ""),
163
+ address,
164
+ question: chatPostCopy(language).changes,
165
+ task: `The previous chat post was cancelled for editing. Apply the member's changes to this draft, then make a NEW chat_post_message call and confirmation. Never reuse the cancelled call. Previous input: ${JSON.stringify(prompt.input)}`
166
+ } : void 0;
133
167
  await source(address, prompt.requestId, decision, {
134
168
  ...principal,
135
169
  attributes: {
136
170
  ...principal.attributes,
137
171
  confirmationRequestId: prompt.requestId,
138
- confirmationDecision: decision
172
+ confirmationDecision: decision,
173
+ ...expired ? { confirmationReason: "expired" } : edit ? { confirmationReason: "edit" } : {}
139
174
  }
140
- });
175
+ }, editQuestion === void 0 ? void 0 : JSON.stringify({ pendingChatPostEdit: editQuestion }));
141
176
  await notice(expired ? "This confirmation expired; nothing executed." : decision === "cancel" ? "Cancellation received." : "Confirmation received.");
142
177
  }
143
- function telegramConfirmationEvents(render = async (description, locale) => fallbackConfirmation(description, locale)) {
178
+ function telegramConfirmationEvents(render = async (description, locale) => fallbackConfirmation(description, locale), archive) {
179
+ const record = async (callId, phase, text, status, ctx) => {
180
+ const saved = confirmations.get()[callId];
181
+ if (archive === void 0 || saved?.description?.chatPost === void 0 || saved.source === void 0) return;
182
+ const operationId = chatPostOperationId(ctx.session.id, callId);
183
+ const source = saved.source;
184
+ const append = () => archive.append(conversationEvent({
185
+ eventId: `chat-post:${operationId}:${phase}`,
186
+ provider: "telegram",
187
+ chatId: source.chatId,
188
+ chatType: source.chatId.startsWith("-") ? "group" : "private",
189
+ threadId: source.threadId || null,
190
+ participantIds: [saved.memberId],
191
+ kind: "tool_result",
192
+ operationId,
193
+ toolCallId: operationId,
194
+ toolName: saved.action,
195
+ sessionId: ctx.session.id,
196
+ text,
197
+ status,
198
+ reason: phase
199
+ }));
200
+ if (phase === "preview") await append();
201
+ else try {
202
+ await append();
203
+ } catch (error) {
204
+ reportTelegramError("chat_post_source_archive_failed", error, {
205
+ operationId,
206
+ phase
207
+ });
208
+ }
209
+ };
144
210
  return {
145
211
  "context.cleared"(_data, channel) {
146
212
  confirmations.update(() => ({}));
@@ -176,18 +242,30 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
176
242
  }
177
243
  const memberId = confirmations.get()[request.action.callId]?.memberId ?? memberIdOf(ctx.session.auth.current);
178
244
  if (memberId === void 0) throw new Error("Confirmation requires a member.");
179
- const copy = await render(confirmations.get()[request.action.callId]?.description, confirmationLocale(ctx.session.auth.current));
245
+ const description = confirmations.get()[request.action.callId]?.description;
246
+ const exactPost = isChatPost(request.action.toolName);
247
+ if (exactPost && description?.chatPost === void 0) throw new Error("An exact chat post cannot show a generic confirmation.");
248
+ const copy = description?.chatPost === void 0 ? await render(description, confirmationLocale(ctx.session.auth.current)) : chatPostCard(description.chatPost, confirmationLocale(ctx.session.auth.current));
180
249
  const text = copy.text;
181
- const sent = await channel.telegram.post({
250
+ await record(request.action.callId, "preview", text, "requested", ctx);
251
+ const send = () => channel.telegram.post({
182
252
  text,
183
- reply_markup: { inline_keyboard: [[{
184
- text: copy.confirm,
185
- callback_data: confirmationCallback(request.requestId, memberId, "approve")
186
- }, {
187
- text: copy.cancel,
188
- callback_data: confirmationCallback(request.requestId, memberId, "cancel")
189
- }]] }
253
+ reply_markup: { inline_keyboard: [[
254
+ {
255
+ text: copy.confirm,
256
+ callback_data: confirmationCallback(request.requestId, memberId, "approve")
257
+ },
258
+ ...exactPost ? [{
259
+ text: chatPostCopy(confirmationLocale(ctx.session.auth.current)).edit,
260
+ callback_data: confirmationCallback(request.requestId, memberId, "edit")
261
+ }] : [],
262
+ {
263
+ text: copy.cancel,
264
+ callback_data: confirmationCallback(request.requestId, memberId, "cancel")
265
+ }
266
+ ]] }
190
267
  });
268
+ const sent = await (exactPost ? withChatPostCard(text, send) : send());
191
269
  const state = channel.state;
192
270
  state.confirmationMessages = {
193
271
  ...state.confirmationMessages,
@@ -196,7 +274,8 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
196
274
  messageId: sent.id,
197
275
  text,
198
276
  copy,
199
- locale: confirmationLocale(ctx.session.auth.current)
277
+ locale: confirmationLocale(ctx.session.auth.current),
278
+ exactPost
200
279
  }
201
280
  };
202
281
  }
@@ -221,12 +300,39 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
221
300
  }
222
301
  const [callId, message] = entry;
223
302
  const copy = message.copy ?? confirmationCopy(message.locale ?? confirmationLocale(ctx.session.auth.current, currentLocale()));
224
- const text = resolution.outcome === "approved" ? copy.executing : copy.cancelled;
225
- await channel.telegram.editMessageText({
303
+ const reason = confirmations.get()[callId]?.cancellationReason;
304
+ const status = resolution.outcome === "approved" ? copy.executing : reason === "expired" ? chatPostCopy(message.locale ?? "en").expired : copy.cancelled;
305
+ const text = message.exactPost ? `${message.text}\n\n${status}` : status;
306
+ const edit = () => channel.telegram.editMessageText({
226
307
  messageId: message.messageId,
227
308
  replyMarkup: { inline_keyboard: [] },
228
309
  text
229
310
  });
311
+ await (message.exactPost ? withChatPostCard(text, edit) : edit());
312
+ await record(callId, reason ?? resolution.outcome, text, resolution.outcome === "approved" ? "completed" : "cancelled", ctx);
313
+ const question = chatPostEditQuestion(resolution);
314
+ if (message.exactPost && reason === "edit" && question !== void 0) {
315
+ pendingQuestions.update((all) => ({
316
+ ...all,
317
+ [questionKey(question)]: question
318
+ }));
319
+ state.pendingQuestions = {
320
+ ...state.pendingQuestions,
321
+ [questionKey(question)]: question
322
+ };
323
+ channel.continuation?.alias(questionAddress(question));
324
+ channel.continuation?.alias(formatAddress({
325
+ ...question,
326
+ conversationId: ""
327
+ }));
328
+ await channel.telegram.post({
329
+ text: question.question,
330
+ reply_markup: {
331
+ force_reply: true,
332
+ selective: true
333
+ }
334
+ });
335
+ }
230
336
  if (resolution.outcome !== "approved") {
231
337
  delete state.confirmationMessages?.[callId];
232
338
  confirmations.update((all) => ({
@@ -239,7 +345,7 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
239
345
  }
240
346
  }
241
347
  },
242
- async "action.result"(data, channel) {
348
+ async "action.result"(data, channel, ctx) {
243
349
  const state = channel.state;
244
350
  if (data.result.kind === "tool-result") {
245
351
  const question = object(data.result.output).pendingQuestion;
@@ -278,7 +384,20 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
278
384
  }
279
385
  const message = state.confirmationMessages?.[data.result.callId];
280
386
  if (message === void 0) return;
281
- if (data.status !== "completed" || data.result.kind === "tool-result" && (data.result.isError === true || object(data.result.output).isError === true || object(object(data.result.output).validation).ok === false)) {
387
+ const failed = data.status !== "completed" || data.result.kind === "tool-result" && (data.result.isError === true || object(data.result.output).isError === true || object(object(data.result.output).validation).ok === false);
388
+ if (message.exactPost) {
389
+ const locale = message.locale ?? currentLocale();
390
+ const receipt = !failed && data.result.kind === "tool-result" ? chatPostReceipt(data.result.output, locale) : void 0;
391
+ const errorId = failed ? reportTelegramError("confirmed_chat_post_failed", data, { callId: data.result.callId }) : void 0;
392
+ const status = receipt ?? (failed ? `${message.copy?.failed ?? "Chat posting failed."} ${errorId}\n${chatPostCopy(locale).uncertain}` : chatPostCopy(locale).uncertain);
393
+ const text = `${message.text}\n\n${status}`;
394
+ await withChatPostCard(text, () => channel.telegram.editMessageText({
395
+ messageId: message.messageId,
396
+ replyMarkup: { inline_keyboard: [] },
397
+ text
398
+ }));
399
+ await record(data.result.callId, "receipt", text, failed ? "failed" : object(data.result.kind === "tool-result" ? data.result.output : void 0).status === "sent" ? "completed" : "unknown", ctx);
400
+ } else if (failed) {
282
401
  const errorId = reportTelegramError("confirmed_action_failed", data, { callId: data.result.callId });
283
402
  await channel.telegram.editMessageText({
284
403
  messageId: message.messageId,
@@ -272,12 +272,14 @@ function captureTelegramFetch(fetchImpl, archive, identities) {
272
272
  }
273
273
  throw error;
274
274
  }
275
- const result = record(await response.clone().json());
275
+ const result = record(await response.clone().json().catch(() => null));
276
+ const messageId = id(record(result.result).message_id);
277
+ const acknowledged = edit || messageId !== null && /^[1-9]\d*$/u.test(messageId);
276
278
  await archive.append({
277
279
  ...event,
278
280
  eventId: `${event.eventId}:result`,
279
- status: response.ok && result.ok === true ? "completed" : "failed",
280
- messageId: id(record(result.result).message_id) ?? event.messageId
281
+ status: response.ok && result.ok === true && acknowledged ? "completed" : result.ok === false ? "failed" : "unknown",
282
+ messageId: messageId ?? event.messageId
281
283
  });
282
284
  return response;
283
285
  };
@@ -1,3 +1,4 @@
1
+ import { type TelegramText } from "./format.js";
1
2
  export declare function withDeliveryLocale<T>(locale: string, run: () => T, threadId?: string): T;
2
3
  export declare function currentLocale(): string;
3
4
  export declare function currentThreadId(): string | undefined;
@@ -16,5 +17,16 @@ export declare function isTechnicalText(text: string): boolean;
16
17
  export declare function reportTelegramError(operation: string, error: unknown, metadata?: Record<string, unknown>, secrets?: ReadonlySet<string>): string;
17
18
  export declare function failureText(errorId: string, kind?: "failed" | "recovery" | "attachment" | "authorization" | "response", locale?: string): string;
18
19
  export declare function customerText(text: string, locale?: string): string;
20
+ /** Check what Telegram will display, including hidden link destinations, after removing markup. */
21
+ export declare function customerMessage(text: string, locale?: string): TelegramText;
22
+ /** Only a definite formatting refusal permits resending; network/unknown failures never do. */
23
+ export declare function formattingRefused(response: Response): Promise<boolean>;
24
+ export declare class TelegramDeliveryError extends Error {
25
+ readonly reference: string;
26
+ readonly delivery: "failed" | "unknown";
27
+ readonly messageIds: readonly number[];
28
+ readonly name = "TelegramDeliveryError";
29
+ constructor(reference: string, delivery: "failed" | "unknown", messageIds: readonly number[]);
30
+ }
19
31
  /** Covers framework defaults, edits and callback notices as well as ordinary agent replies. */
20
- export declare function safeTelegramFetch(fetchImpl: typeof fetch): typeof fetch;
32
+ export declare function safeTelegramFetch(fetchImpl: typeof fetch, scope?: string): typeof fetch;