@segmentstream/agent 0.6.8 → 0.6.11

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 (31) hide show
  1. package/README.md +18 -4
  2. package/dist/_chunks/conversation-runtime-ovs8IhXJ.mjs +212 -0
  3. package/dist/_chunks/inbound-request-DRNPIIpb.mjs +325 -0
  4. package/dist/extension/hooks/conversations.mjs +1 -1
  5. package/dist/extension/instructions/conversations.mjs +1 -1
  6. package/dist/extension/instructions.md +4 -0
  7. package/dist/extension/lib/conversation-runtime.mjs +1 -109
  8. package/dist/extension/lib/conversations.d.ts +13 -2
  9. package/dist/extension/lib/conversations.mjs +5 -5
  10. package/dist/extension/lib/telegram/adapter.mjs +20 -6
  11. package/dist/extension/lib/telegram/attachments-send.mjs +1 -1
  12. package/dist/extension/lib/telegram/audience.d.ts +5 -1
  13. package/dist/extension/lib/telegram/audience.mjs +15 -2
  14. package/dist/extension/lib/telegram/confirmations.mjs +1 -2
  15. package/dist/extension/lib/telegram/conversations.d.ts +2 -2
  16. package/dist/extension/lib/telegram/conversations.mjs +110 -26
  17. package/dist/extension/lib/telegram/customer-text.d.ts +1 -1
  18. package/dist/extension/lib/telegram/customer-text.mjs +1 -217
  19. package/dist/extension/lib/telegram/files.mjs +1 -1
  20. package/dist/extension/lib/telegram/images.mjs +1 -1
  21. package/dist/extension/lib/telegram/inbound-request.d.ts +7 -0
  22. package/dist/extension/lib/telegram/inbound-request.mjs +1 -74
  23. package/dist/extension/lib/telegram/inbound.mjs +15 -1
  24. package/dist/extension/lib/telegram/media-policy.mjs +1 -1
  25. package/dist/extension/lib/telegram/router.d.ts +2 -0
  26. package/dist/extension/lib/telegram/router.mjs +4 -3
  27. package/dist/extension/lib/telegram/silent.mjs +1 -1
  28. package/dist/extension/lib/telegram/turn-errors.d.ts +1 -1
  29. package/dist/extension/lib/telegram/turn-errors.mjs +9 -1
  30. package/dist/extension/lib/telegram/voice.mjs +1 -1
  31. package/package.json +1 -1
package/README.md CHANGED
@@ -381,11 +381,25 @@ Apache-2.0
381
381
 
382
382
  Production Telegram webhooks archive every receivable message in the platform database,
383
383
  including automated posts and unanswered messages. The extension also records replies,
384
- reply/silent decisions, tool names and redacted argument summaries, results' status, and
384
+ reply/silent decisions, tool names, redacted arguments/results/errors, and
385
385
  attachment references. It reloads stored context for the same chat/topic each turn, including
386
- across cold starts and compaction. File bytes and tool output bodies are not archived.
387
-
388
- Retention defaults to 365 days per workspace. Platform REST reads, the workspace ontology's
386
+ across cold starts and compaction. File bytes are replaced by markers.
387
+
388
+ The Eve hook sanitizes tool arguments, results and failure messages in `argsText`,
389
+ `resultText` and `errorText`; ingestion re-sanitizes them. UTF-8 caps are 16/32/4 KiB, with explicit JSON
390
+ truncation markers. Content and argument summaries expire within 30 days of the original event
391
+ timestamp (clamped to server time); replay cannot renew that window after archive deletion.
392
+ Embedded JSON objects and arrays are sanitized recursively. Strings under text/content/body/csv/tsv
393
+ and file/email body fields are omitted unless they parse as JSON objects or arrays. This includes
394
+ all CSV/TSV documents, with or without headers. `argsSummary` keeps its pre-log semantics: the SDK
395
+ applies bounded credential and file-byte redaction, and ingestion preserves the supplied summary.
396
+ Deploy backend migration 0043/API before upgrading to agent SDK 0.6.10.
397
+ `segmentstream agent logs` and its `show`/`call` commands read this content
398
+ with a human Cloud session; all members with workspace access can read DMs too.
399
+ Runtime/model/MCP conversation readers never receive these new fields.
400
+
401
+ Archive metadata and message text retain the workspace policy (365 days by default).
402
+ Platform REST reads, the workspace ontology's
389
403
  `chat_history` and `chat_search`, and the member REST facade enforce workspace/group
390
404
  permissions and DM participant/admin scope. Deploy the backend migration/API and upgrade the
391
405
  ontology SDK along with this extension. See the repository's
@@ -0,0 +1,212 @@
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 { memberIdOf } from "../extension/lib/telegram/principal.mjs";
8
+ import { conversationEvent, conversations, summarizeArguments } from "../extension/lib/conversations.mjs";
9
+ import { parseAddress } from "../extension/lib/telegram/sessions.mjs";
10
+ import { completionKind } from "../extension/lib/telegram/silent.mjs";
11
+ const CONTENT_CAPS = {
12
+ args: 16384,
13
+ result: 32768,
14
+ error: 4096
15
+ };
16
+ const sensitive = /token|password|passwd|secret|authorization|cookie|credential|api.?key|private.?key|session|iban|card|cvv|ssn|email|phone|address|birth|document.?text|raw.?content|body|bytes|base64/i;
17
+ const encoder = new TextEncoder();
18
+ function luhn(value) {
19
+ const digits = value.replace(/\D/g, "");
20
+ if (digits.length < 13 || digits.length > 19) return false;
21
+ let sum = 0;
22
+ let double = false;
23
+ for (let i = digits.length - 1; i >= 0; i--) {
24
+ let n = Number(digits[i]);
25
+ if (double && (n *= 2) > 9) n -= 9;
26
+ sum += n;
27
+ double = !double;
28
+ }
29
+ return sum % 10 === 0;
30
+ }
31
+ function redactText(value) {
32
+ return value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, "[binary data]").replace(/-----BEGIN [^-\r\n]+-----[\s\S]*?(?:-----END [^-\r\n]+-----|$)/g, "[redacted PEM]").replace(/data:[^\s"']+/gi, "[binary data]").replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9+/_=.:~\-]+/gi, "[redacted authorization]").replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[redacted JWT]").replace(/\b(?:ssw_|sscli_|sk[-_]|rk[-_])[A-Za-z0-9._~\-]+/gi, "[redacted key]").replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1[redacted]@").replace(/([?&](?:[^=&#\s]*(?:token|password|secret|key|signature|credential|session|auth|cookie)[^=&#\s]*|sig)=)[^&#\s"']*/gi, "$1[redacted]").replace(/((?:["']?)(?:[\w-]*(?:token|password|passwd|secret|authorization|cookie|credential|api.?key|private.?key|session|iban|card|cvv|ssn)[\w-]*)(?:["']?)\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;})]+)/gi, "$1[redacted]").replace(/\b[A-Z]{2}\d{2}(?:[ -]?[A-Z0-9]){11,30}\b/gi, "[redacted IBAN]").replace(/\b\d(?:[ -]?\d){12,18}\b/g, (candidate) => luhn(candidate) ? "[redacted card]" : candidate).replace(/[A-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Z0-9](?:[A-Z0-9.-]*[A-Z0-9])?\.[A-Z]{2,}/gi, "[email]").replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[redacted SSN]").replace(/\b[A-Za-z0-9+/]{128,}={0,2}/g, "[binary data]");
33
+ }
34
+ const documentField = /^(?:text|content|body|csv|tsv|file[_-]?body|email[_-]?body)$/i;
35
+ const omittedDocument = "[omitted: unstructured document content]";
36
+ function redactContent(value) {
37
+ const seen = new WeakSet();
38
+ let nodes = 0;
39
+ const visit = (item, depth, field) => {
40
+ if (++nodes > 5e3 || depth > 12) return "[omitted: content complexity]";
41
+ if (typeof item === "string") {
42
+ try {
43
+ const embedded = JSON.parse(item);
44
+ if (embedded !== null && typeof embedded === "object") return JSON.stringify(visit(embedded, depth + 1, field));
45
+ } catch {}
46
+ if (documentField.test(field ?? "")) return omittedDocument;
47
+ return redactText(item);
48
+ }
49
+ if (typeof item === "bigint") return item.toString();
50
+ if (item === null || typeof item !== "object") return item;
51
+ if (ArrayBuffer.isView(item) || item instanceof ArrayBuffer) return "[binary data]";
52
+ if (seen.has(item)) return "[omitted: circular]";
53
+ seen.add(item);
54
+ if (Array.isArray(item)) {
55
+ const entries = item.slice(0, 200).map((entry) => visit(entry, depth + 1, field));
56
+ if (item.length > 200) entries.push("[omitted: array entries]");
57
+ return entries;
58
+ }
59
+ const entries = Object.entries(item);
60
+ return Object.fromEntries([...entries.slice(0, 200).map(([key, entry]) => [redactText(key), sensitive.test(key) && !documentField.test(key) ? "[redacted]" : visit(entry, depth + 1, key)]), ...entries.length > 200 ? [["_omitted", "[omitted: object entries]"]] : []]);
61
+ };
62
+ return visit(value, 0);
63
+ }
64
+ function contentText(value, cap) {
65
+ if (value == null) return null;
66
+ let parsed = value;
67
+ if (typeof value === "string") try {
68
+ parsed = JSON.parse(value);
69
+ } catch {}
70
+ const redacted = redactContent(parsed);
71
+ const text = typeof redacted === "string" ? redacted : JSON.stringify(redacted) ?? "[omitted]";
72
+ if (encoder.encode(text).length <= cap) return text;
73
+ let preview = text;
74
+ while (true) {
75
+ const result = JSON.stringify({
76
+ truncated: true,
77
+ preview
78
+ });
79
+ const bytes = encoder.encode(result).length;
80
+ if (bytes <= cap) return result;
81
+ const available = Math.max(0, encoder.encode(preview).length - (bytes - cap) - 4);
82
+ preview = new TextDecoder().decode(encoder.encode(preview).slice(0, available), { stream: true });
83
+ }
84
+ }
85
+ function conversationCoordinates(context) {
86
+ const principal = context.session.auth.current;
87
+ const telegram = principal?.authenticator === "segmentstream-telegram" || context.channel.kind?.includes("telegram");
88
+ const address = context.channel.continuationToken;
89
+ const attributes = principal?.attributes;
90
+ const fields = address?.split(":");
91
+ const localAddress = telegram && fields?.length === 4 ? fields.slice(1).join(":") : address;
92
+ const parsed = telegram && localAddress !== void 0 && /^-?\d+:\d*:\d*$/u.test(localAddress) ? parseAddress(localAddress) : void 0;
93
+ const attributeChatId = typeof attributes?.chatId === "string" && (!telegram || /^-?\d+$/u.test(attributes.chatId)) ? attributes.chatId : void 0;
94
+ const chatId = telegram ? parsed?.chatId ?? attributeChatId : attributeChatId !== void 0 ? attributeChatId : address;
95
+ if (!chatId) return;
96
+ const memberId = memberIdOf(principal);
97
+ const chatType = attributes?.chatType;
98
+ return {
99
+ provider: telegram ? "telegram" : context.channel.kind ?? "eve",
100
+ chatId,
101
+ chatType: chatType === "group" || chatType === "supergroup" || chatType === void 0 && telegram && chatId.startsWith("-") ? "group" : chatType === "channel" ? "channel" : "private",
102
+ threadId: !telegram ? null : parsed !== void 0 ? parsed.messageThreadId || null : typeof attributes?.messageThreadId === "string" ? attributes.messageThreadId : null,
103
+ participantIds: memberId === void 0 ? [] : [memberId],
104
+ senderId: principal?.subject ?? null
105
+ };
106
+ }
107
+ function conversationRecorder(archive = conversations()) {
108
+ return async (event, context) => {
109
+ const coordinates = conversationCoordinates(context);
110
+ if (coordinates === void 0) return;
111
+ const base = {
112
+ ...coordinates,
113
+ sessionId: context.session.id,
114
+ turnId: event.data !== void 0 && "turnId" in event.data ? event.data.turnId : context.session.turn.id,
115
+ at: event.meta.at
116
+ };
117
+ const eventId = `eve:${context.session.id}:${event.meta.id}`;
118
+ if (event.type === "actions.requested") for (const action of event.data.actions) {
119
+ const name = "toolName" in action ? action.toolName : action.name;
120
+ await archive.append(conversationEvent({
121
+ ...base,
122
+ eventId: `${eventId}:${action.callId}`,
123
+ kind: "tool_call",
124
+ toolCallId: action.callId,
125
+ toolName: name,
126
+ argsSummary: summarizeArguments(action.input),
127
+ argsText: contentText(action.input, CONTENT_CAPS.args),
128
+ status: "requested"
129
+ }));
130
+ }
131
+ else if (event.type === "action.result") {
132
+ const result = event.data.result;
133
+ const name = "toolName" in result ? result.toolName : "name" in result ? result.name : result.subagentName;
134
+ await archive.append(conversationEvent({
135
+ ...base,
136
+ eventId,
137
+ kind: "tool_result",
138
+ toolCallId: result.callId,
139
+ toolName: name,
140
+ status: event.data.status,
141
+ resultText: contentText("output" in result ? result.output : null, CONTENT_CAPS.result),
142
+ errorText: contentText(event.data.error?.message ?? (event.data.status === "failed" ? resultErrorMessage(result.output) : null), CONTENT_CAPS.error)
143
+ }));
144
+ } else if (event.type === "task.started" || event.type === "task.settled") await archive.append(conversationEvent({
145
+ ...base,
146
+ eventId,
147
+ kind: event.type === "task.started" ? "tool_call" : "tool_result",
148
+ toolCallId: event.data.callId,
149
+ toolName: event.data.name ?? "task",
150
+ argsSummary: "{}",
151
+ ...event.type === "task.settled" ? {
152
+ resultText: contentText(event.data.output, CONTENT_CAPS.result),
153
+ errorText: contentText(event.data.error?.message, CONTENT_CAPS.error)
154
+ } : {},
155
+ status: event.type === "task.started" ? "requested" : event.data.status === "cancelled" ? "cancelled" : event.data.status === "failed" ? "failed" : "completed"
156
+ }));
157
+ else if (event.type === "turn.failed" || event.type === "step.failed") await archive.append(conversationEvent({
158
+ ...base,
159
+ turnId: event.data.turnId,
160
+ eventId,
161
+ kind: "decision",
162
+ decision: "silent",
163
+ reason: event.type,
164
+ status: "failed",
165
+ errorText: contentText(event.data.message, CONTENT_CAPS.error)
166
+ }));
167
+ else if (event.type === "message.completed" && event.data.finishReason !== "tool-calls") {
168
+ const kind = completionKind(event.data.message, event.data.finishReason);
169
+ const silent = kind !== "reply";
170
+ const note = kind === "silent_marker" ? "Model returned an explicit silence marker; no message delivery." : kind === "empty" ? "Model completed without final text; no message delivery." : kind === "whitespace" ? "Model returned only whitespace; no message delivery." : "Model produced a reply; transport delivery is recorded separately.";
171
+ await archive.append(conversationEvent({
172
+ ...base,
173
+ eventId,
174
+ kind: "decision",
175
+ decision: silent ? "silent" : "reply",
176
+ reason: kind === "reply" ? "model_reply" : `model_${kind}_output`,
177
+ text: note,
178
+ argsSummary: JSON.stringify({
179
+ outputKind: kind,
180
+ outputCharacters: event.data.message?.length ?? 0,
181
+ finishReason: event.data.finishReason
182
+ })
183
+ }));
184
+ if (coordinates.provider !== "telegram" && !silent) await archive.append(conversationEvent({
185
+ ...base,
186
+ eventId: `${eventId}:outbound`,
187
+ kind: "outbound",
188
+ text: event.data.message,
189
+ senderId: "agent",
190
+ senderIsBot: true,
191
+ status: "completed"
192
+ }));
193
+ } else if (event.type === "message.received" && coordinates.provider !== "telegram") await archive.append(conversationEvent({
194
+ ...base,
195
+ eventId,
196
+ kind: "inbound",
197
+ text: event.data.message,
198
+ attachments: (event.data.parts ?? []).filter((part) => part.type === "file").map((part, index) => ({
199
+ id: `${eventId}:file:${index}`,
200
+ kind: "file",
201
+ ...part.filename ? { filename: part.filename } : {},
202
+ contentType: part.mediaType
203
+ }))
204
+ }));
205
+ };
206
+ }
207
+ function resultErrorMessage(output) {
208
+ if (typeof output === "string") return output;
209
+ if (output !== null && typeof output === "object" && "message" in output && typeof output.message === "string") return output.message;
210
+ return null;
211
+ }
212
+ export { conversationRecorder as n, conversationCoordinates as t };
@@ -0,0 +1,325 @@
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 { isExactChatPostCard } from "../extension/lib/telegram/chat-post-card.mjs";
8
+ import { redactDiagnostic } from "../extension/lib/diagnostics.mjs";
9
+ import { plainTelegramText, splitTelegramText, telegramMarkdown } from "../extension/lib/telegram/format.mjs";
10
+ import { inTelegramSendQueue } from "../extension/lib/telegram/send-queue.mjs";
11
+ import { randomBytes } from "node:crypto";
12
+ import { AsyncLocalStorage } from "node:async_hooks";
13
+ const deliveryLocale = new AsyncLocalStorage();
14
+ function withDeliveryLocale(locale, run, threadId) {
15
+ return deliveryLocale.run({
16
+ locale,
17
+ threadId
18
+ }, run);
19
+ }
20
+ function currentLocale() {
21
+ return deliveryLocale.getStore()?.locale ?? currentInboundRequest()?.locale ?? "en";
22
+ }
23
+ function currentThreadId() {
24
+ return deliveryLocale.getStore()?.threadId ?? currentInboundRequest()?.threadId;
25
+ }
26
+ function customerCopy(locale = currentLocale()) {
27
+ return locale.toLowerCase().split(/[-_]/)[0] === "ru" ? {
28
+ failed: "Не удалось выполнить запрос. Попробуйте ещё раз.",
29
+ recovery: "Не удалось продолжить разговор. Отправьте новое сообщение.",
30
+ attachment: "Не удалось обработать файл. Попробуйте ещё раз.",
31
+ authorization: "Не удалось подключиться. Попробуйте ещё раз.",
32
+ response: "Не удалось показать ответ. Попробуйте ещё раз.",
33
+ reference: "Код ошибки",
34
+ connected: "Доступ подключён.",
35
+ disconnected: "Подключение отменено."
36
+ } : {
37
+ failed: "I couldn't complete the request. Please try again.",
38
+ recovery: "I couldn't continue the conversation. Please send a new message.",
39
+ attachment: "I couldn't process the file. Please try again.",
40
+ authorization: "I couldn't connect. Please try again.",
41
+ response: "I couldn't display the response. Please try again.",
42
+ reference: "Error reference",
43
+ connected: "Access connected.",
44
+ disconnected: "Connection cancelled."
45
+ };
46
+ }
47
+ const TECHNICAL_PATTERNS = [
48
+ /[{}]|```/u,
49
+ /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/iu,
50
+ /\b[0-9a-hjkmnp-tv-z]{26}\b/iu,
51
+ /\b\w+__\w+/u,
52
+ /\b(?:Serialization|Type|Reference|Syntax|Tool|Provider|API|Workflow|InternalServer)Error\b/u,
53
+ /\bError:\s|(?:^|\n)\s*at\s+\S+|\b(?:stack trace|traceback|SQLSTATE|node_modules)\b/imu,
54
+ /\b(?:duplicate key|ECONN\w+|HTTP [45]\d\d)\b/iu,
55
+ /\[\s*(?:"[^"]*"|\d+|true|false|null)\s*[,\]]/u
56
+ ];
57
+ function isTechnicalText(text) {
58
+ if (/https?:\/\/[^\s/@]+:[^\s/@]+@|[?&](?:token|access_token|api_key|secret|password)=/iu.test(text)) return true;
59
+ const visible = text.replace(/https?:\/\/[^\s)<>]+/giu, "link");
60
+ const prose = visible.replace(/(?<![\w@])@[a-z][a-z0-9_]{4,31}(?!\w)/giu, "recipient");
61
+ return TECHNICAL_PATTERNS.some((pattern) => pattern.test(visible)) || /\b[a-z][a-z0-9]*_[a-z][a-z0-9_]*\b(?!\.[a-z0-9])/iu.test(prose);
62
+ }
63
+ function redactCredentials(value) {
64
+ if (typeof value === "string") return value.replace(/\bBearer [A-Za-z0-9_.~+/-]+=*/giu, "Bearer [redacted]").replace(/(https?:\/\/)[^/@\s]+:[^/@\s]+@/gu, "$1[redacted]@").replace(/([?&](?:token|access_token|api_key|secret|password)=)[^&#\s]*/giu, "$1[redacted]").replace(/("(?:token|api_key|password|secret)"\s*:\s*)"(?:\\.|[^"\\])*"/giu, "$1\"[redacted]\"");
65
+ if (Array.isArray(value)) return value.map(redactCredentials);
66
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, /^(?:token|password|secret|authorization|cookie|api_?key|access_?token|refresh_?token)$/iu.test(key) && typeof item === "string" ? "[redacted]" : redactCredentials(item)]));
67
+ return value;
68
+ }
69
+ function reportTelegramError(operation, error, metadata = {}, secrets = new Set()) {
70
+ const errorId = randomBytes(4).toString("hex").toUpperCase();
71
+ const credentials = new Set([...secrets, ...[process.env.AI_GATEWAY_API_KEY, process.env.VERCEL_OIDC_TOKEN].filter((value) => value !== void 0)]);
72
+ console.error("[segmentstream] Telegram delivery diagnostic", {
73
+ ...metadata,
74
+ errorId,
75
+ operation,
76
+ error: redactCredentials(redactDiagnostic(error, credentials))
77
+ });
78
+ return errorId;
79
+ }
80
+ function failureText(errorId, kind = "failed", locale = currentLocale()) {
81
+ if (kind === "cancelled") return `⏹ ${errorId}`;
82
+ const copy = customerCopy(locale);
83
+ return `${copy[kind]} ${copy.reference}: ${errorId}.`;
84
+ }
85
+ function customerText(text, locale = currentLocale()) {
86
+ if (!isTechnicalText(text)) return text;
87
+ return failureText(reportTelegramError("technical_text_blocked", { text }), "response", locale);
88
+ }
89
+ function customerMessage(text, locale = currentLocale()) {
90
+ let message;
91
+ try {
92
+ message = telegramMarkdown(text);
93
+ } catch (error) {
94
+ reportTelegramError("markdown_render_failed", error);
95
+ message = {
96
+ text: plainTelegramText(text),
97
+ entities: []
98
+ };
99
+ }
100
+ const links = message.entities.flatMap((entity) => entity.url === void 0 ? [] : [entity.url]);
101
+ const visible = customerText(message.text, locale);
102
+ if (visible !== message.text) return {
103
+ text: visible,
104
+ entities: []
105
+ };
106
+ if (links.some(isTechnicalText)) return {
107
+ text: failureText(reportTelegramError("unsafe_markdown_link", new Error("Credential-bearing link refused.")), "response", locale),
108
+ entities: []
109
+ };
110
+ return message;
111
+ }
112
+ async function formattingRefused(response) {
113
+ if (response.status !== 400) return false;
114
+ const body = await response.clone().json().catch(() => null);
115
+ return body?.ok === false && typeof body.description === "string" && (/can't parse entities|cannot parse entities|entity (?:bounds|offset|length)|entities (?:are|too)/iu.test(body.description) || /unsupported (?:start tag|parse mode)/iu.test(body.description));
116
+ }
117
+ var TelegramDeliveryError = class extends Error {
118
+ reference;
119
+ delivery;
120
+ messageIds;
121
+ name = "TelegramDeliveryError";
122
+ constructor(reference, delivery, messageIds) {
123
+ super(`Telegram delivery ${delivery}; reference ${reference}. Do not automatically resend.`);
124
+ this.reference = reference;
125
+ this.delivery = delivery;
126
+ this.messageIds = messageIds;
127
+ }
128
+ };
129
+ function safeTelegramFetch(fetchImpl, scope = "telegram") {
130
+ const methods = new Set([
131
+ "sendMessage",
132
+ "editMessageText",
133
+ "editMessageCaption",
134
+ "answerCallbackQuery",
135
+ "sendPhoto",
136
+ "sendDocument",
137
+ "sendMediaGroup"
138
+ ]);
139
+ return async (input, init) => {
140
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
141
+ if (!methods.has(url.split("/").at(-1) ?? "") || typeof init?.body !== "string") return fetchImpl(input, init);
142
+ const body = JSON.parse(init.body);
143
+ for (const field of ["text", "caption"]) {
144
+ if (typeof body[field] === "string" && isExactChatPostCard(body[field])) body.parse_mode = "MarkdownV2";
145
+ if (typeof body[field] === "string" && !isExactChatPostCard(body[field])) {
146
+ const entityField = field === "text" ? "entities" : "caption_entities";
147
+ const existing = body[entityField];
148
+ const message = Array.isArray(existing) ? {
149
+ text: body[field],
150
+ entities: existing
151
+ } : customerMessage(body[field]);
152
+ const guarded = customerText(message.text);
153
+ body[field] = message.entities.some((entity) => typeof entity.url === "string" && isTechnicalText(entity.url)) ? failureText(reportTelegramError("unsafe_entity_link", new Error("Credential-bearing link refused.")), "response") : guarded;
154
+ if (!url.endsWith("/answerCallbackQuery")) body[entityField] = body[field] === message.text ? message.entities : [];
155
+ delete body.parse_mode;
156
+ }
157
+ }
158
+ if (Array.isArray(body.media)) body.media = body.media.map((item) => ({
159
+ ...item,
160
+ ...typeof item.caption === "string" ? { caption: customerText(item.caption) } : {}
161
+ }));
162
+ const markup = body.reply_markup;
163
+ if (Array.isArray(markup?.inline_keyboard)) markup.inline_keyboard = markup.inline_keyboard.map((row) => row.map((button) => {
164
+ if (typeof button.text !== "string" || !isTechnicalText(button.text)) return button;
165
+ reportTelegramError("technical_button_label_blocked", { text: button.text });
166
+ return {
167
+ ...button,
168
+ text: currentLocale().startsWith("ru") ? "Продолжить" : "Continue"
169
+ };
170
+ }));
171
+ const send = async () => {
172
+ const parts = url.endsWith("/sendMessage") && typeof body.text === "string" && !isExactChatPostCard(body.text) ? splitTelegramText({
173
+ text: body.text,
174
+ entities: body.entities ?? []
175
+ }) : [null];
176
+ let first;
177
+ const messageIds = [];
178
+ const attempt = async (outgoing) => {
179
+ try {
180
+ return await fetchImpl(input, {
181
+ ...init,
182
+ body: JSON.stringify(outgoing)
183
+ });
184
+ } catch (error) {
185
+ throw new TelegramDeliveryError(reportTelegramError("telegram_send_unknown", error, { messageIds }), "unknown", messageIds);
186
+ }
187
+ };
188
+ for (const [index, part] of parts.entries()) {
189
+ const outgoing = part === null ? { ...body } : {
190
+ ...body,
191
+ text: part.text,
192
+ entities: part.entities
193
+ };
194
+ if (index < parts.length - 1) delete outgoing.reply_markup;
195
+ let response = await attempt(outgoing);
196
+ const entityField = typeof outgoing.text === "string" ? "entities" : "caption_entities";
197
+ if (Array.isArray(outgoing[entityField]) && outgoing[entityField].length > 0 && await formattingRefused(response)) {
198
+ delete outgoing[entityField];
199
+ delete outgoing.parse_mode;
200
+ response = await attempt(outgoing);
201
+ }
202
+ const result = await response.clone().json().catch(() => null);
203
+ if (url.endsWith("/sendMessage")) {
204
+ const id = result?.result?.message_id;
205
+ if (!response.ok || result?.ok !== true || typeof id !== "number" || !Number.isSafeInteger(id) || id < 1) {
206
+ const delivery = result?.ok === false ? "failed" : "unknown";
207
+ throw new TelegramDeliveryError(reportTelegramError("telegram_send_unacknowledged", {
208
+ status: response.status,
209
+ result,
210
+ messageIds,
211
+ delivery
212
+ }), delivery, messageIds);
213
+ }
214
+ messageIds.push(id);
215
+ } else if (!response.ok || result?.ok !== true) return response;
216
+ first ??= response;
217
+ }
218
+ if (first === void 0) throw new Error("Telegram message has no visible text; nothing was sent.");
219
+ return first;
220
+ };
221
+ return typeof body.chat_id === "string" || typeof body.chat_id === "number" ? inTelegramSendQueue(scope, String(body.chat_id), send) : send();
222
+ };
223
+ }
224
+ var InboundRequest = class {
225
+ agentDescription;
226
+ edited = false;
227
+ deliveryFailed;
228
+ claimTurn;
229
+ beginDispatch;
230
+ finishClaim;
231
+ locale;
232
+ threadId;
233
+ text;
234
+ files;
235
+ extraFiles;
236
+ sessions;
237
+ waitUntil;
238
+ address;
239
+ respond;
240
+ constructor(agentDescription) {
241
+ this.agentDescription = agentDescription;
242
+ }
243
+ };
244
+ const storage = new AsyncLocalStorage();
245
+ function runInboundRequest(request, run) {
246
+ return storage.run(request, run);
247
+ }
248
+ function currentInboundRequest() {
249
+ return storage.getStore();
250
+ }
251
+ function replaceText(message, text) {
252
+ if (typeof message === "string") return text;
253
+ return [{
254
+ type: "text",
255
+ text
256
+ }, ...message.filter((part) => part.type !== "text")];
257
+ }
258
+ function telegramFileId(data) {
259
+ const href = data instanceof URL ? data.href : typeof data === "string" ? data : void 0;
260
+ if (href === void 0 || !href.startsWith("telegram-file:")) return;
261
+ try {
262
+ return decodeURIComponent(new URL(href).pathname);
263
+ } catch {
264
+ return;
265
+ }
266
+ }
267
+ function replaceFiles(message, files) {
268
+ if (typeof message === "string") return message;
269
+ return message.map((part) => {
270
+ if (part.type !== "file") return part;
271
+ const fileId = telegramFileId(part.data);
272
+ const staged = fileId === void 0 ? void 0 : files.get(fileId);
273
+ if (staged === void 0) return part;
274
+ if (staged.status === "refused") return {
275
+ type: "text",
276
+ text: `[file ${staged.filename} was not received: ${staged.reason}]`
277
+ };
278
+ if (staged.imageInput === "text") return {
279
+ type: "text",
280
+ text: `[image ${staged.attachment.filename}: ${staged.description ?? "see attachment context"}]`
281
+ };
282
+ return {
283
+ ...part,
284
+ data: staged.url
285
+ };
286
+ });
287
+ }
288
+ function withReplacedText(source, request) {
289
+ return {
290
+ ...source,
291
+ async send(message, options) {
292
+ const text = request.text === void 0 ? message : replaceText(message, request.text);
293
+ const files = request.files === void 0 ? text : replaceFiles(text, request.files);
294
+ const content = request.extraFiles === void 0 || request.extraFiles.length === 0 ? files : [...typeof files === "string" ? [{
295
+ type: "text",
296
+ text: files
297
+ }] : files, ...request.extraFiles];
298
+ let result;
299
+ try {
300
+ result = await source.send(content, {
301
+ ...options,
302
+ turnPolicy: "queue"
303
+ });
304
+ } catch (error) {
305
+ try {
306
+ await request.finishClaim?.(false);
307
+ await request.deliveryFailed?.(error);
308
+ } catch (cleanupError) {
309
+ console.error("[segmentstream] inbound delivery cleanup failed", {
310
+ cleanupError,
311
+ cause: error
312
+ });
313
+ }
314
+ throw error;
315
+ }
316
+ try {
317
+ await request.finishClaim?.(true);
318
+ } catch (error) {
319
+ reportTelegramError("turn_admission_recording_failed", error);
320
+ }
321
+ return result;
322
+ }
323
+ };
324
+ }
325
+ export { TelegramDeliveryError as a, customerCopy as c, failureText as d, formattingRefused as f, withDeliveryLocale as g, safeTelegramFetch as h, withReplacedText as i, customerMessage as l, reportTelegramError as m, currentInboundRequest as n, currentLocale as o, isTechnicalText as p, runInboundRequest as r, currentThreadId as s, InboundRequest as t, customerText as u };
@@ -4,7 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
4
4
  const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
- import { conversationRecorder } from "../lib/conversation-runtime.mjs";
7
+ import { n as conversationRecorder } from "../../_chunks/conversation-runtime-ovs8IhXJ.mjs";
8
8
  import { defineHook } from "eve/hooks";
9
9
  var conversations_default = defineHook({ events: { "*": conversationRecorder() } });
10
10
  export { conversations_default as default };
@@ -5,7 +5,7 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { conversations, storedConversationContext } from "../lib/conversations.mjs";
8
- import { conversationCoordinates } from "../lib/conversation-runtime.mjs";
8
+ import { t as conversationCoordinates } from "../../_chunks/conversation-runtime-ovs8IhXJ.mjs";
9
9
  import { defineDynamic, defineInstructions } from "eve/instructions";
10
10
  const archive = conversations();
11
11
  var conversations_default = defineDynamic({ events: { "turn.started": async (_event, context) => {
@@ -19,6 +19,10 @@ talking to you, and you act with exactly their permissions.
19
19
  - Ask only when information is missing or ambiguous. In Telegram, use
20
20
  `segmentstream__questions_ask` with the question and pending task so the session keeps its intent,
21
21
  member and chat/topic. An answer to a pending question is never chatter or `[SILENT]`.
22
+ - New messages and uploads may arrive together or while you are working. Complete or explicitly
23
+ report the status of every request and every supplied file. A later message adds work unless
24
+ the member explicitly cancels or replaces the earlier task. If work is interrupted or blocked,
25
+ say what remains unfinished; never silently replace it with the latest question.
22
26
  - Destructive or irreversible actions, and legacy custom actions without safety labels, require
23
27
  the tool boundary's Confirm / Cancel buttons. Call the action to start that flow; never ask for
24
28
  approval in prose or treat a text acknowledgement as approval. Wait for the button outcome.