@segmentstream/agent 0.8.0 → 0.8.2

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 (49) hide show
  1. package/README.md +52 -0
  2. package/dist/_chunks/build-metadata-DMTUHsRx.mjs +16 -0
  3. package/dist/_chunks/{inbound-request-BwBvXk0A.mjs → inbound-request-CYSoY6pu.mjs} +35 -5
  4. package/dist/extension/config.d.ts +3 -0
  5. package/dist/extension/config.mjs +17 -2
  6. package/dist/extension/lib/attachment-tools.d.ts +5 -4
  7. package/dist/extension/lib/attachment-tools.mjs +3 -3
  8. package/dist/extension/lib/build-metadata.d.ts +8 -0
  9. package/dist/extension/lib/build-metadata.mjs +8 -0
  10. package/dist/extension/lib/confirmations.mjs +1 -1
  11. package/dist/extension/lib/deferred-confirmations.mjs +2 -2
  12. package/dist/extension/lib/ontology-tools.d.ts +9 -2
  13. package/dist/extension/lib/ontology-tools.mjs +48 -7
  14. package/dist/extension/lib/questions.d.ts +2 -0
  15. package/dist/extension/lib/stored-files.d.ts +17 -0
  16. package/dist/extension/lib/stored-files.mjs +70 -0
  17. package/dist/extension/lib/telegram/adapter.mjs +58 -4
  18. package/dist/extension/lib/telegram/attachment-description.mjs +1 -1
  19. package/dist/extension/lib/telegram/attachments-send.mjs +3 -3
  20. package/dist/extension/lib/telegram/confirmation-expiry.mjs +2 -2
  21. package/dist/extension/lib/telegram/confirmation-text.mjs +1 -1
  22. package/dist/extension/lib/telegram/confirmations.mjs +2 -2
  23. package/dist/extension/lib/telegram/content.d.ts +1 -1
  24. package/dist/extension/lib/telegram/conversations.mjs +1 -1
  25. package/dist/extension/lib/telegram/customer-text.d.ts +1 -1
  26. package/dist/extension/lib/telegram/customer-text.mjs +1 -1
  27. package/dist/extension/lib/telegram/deferred-confirmations.mjs +2 -2
  28. package/dist/extension/lib/telegram/files.mjs +1 -1
  29. package/dist/extension/lib/telegram/health.mjs +3 -1
  30. package/dist/extension/lib/telegram/images.mjs +1 -1
  31. package/dist/extension/lib/telegram/inbound-request.d.ts +11 -0
  32. package/dist/extension/lib/telegram/inbound-request.mjs +1 -1
  33. package/dist/extension/lib/telegram/inbound.d.ts +2 -0
  34. package/dist/extension/lib/telegram/inbound.mjs +146 -4
  35. package/dist/extension/lib/telegram/lifecycle.d.ts +46 -0
  36. package/dist/extension/lib/telegram/lifecycle.mjs +163 -0
  37. package/dist/extension/lib/telegram/media-policy.mjs +1 -1
  38. package/dist/extension/lib/telegram/silent.mjs +1 -1
  39. package/dist/extension/lib/telegram/turn-errors.mjs +1 -1
  40. package/dist/extension/lib/telegram/voice.mjs +1 -1
  41. package/dist/extension/tools/attachments_discard.d.ts +4 -4
  42. package/dist/extension/tools/attachments_list.d.ts +4 -4
  43. package/dist/extension/tools/attachments_promote.d.ts +4 -4
  44. package/dist/extension/tools/attachments_promote.mjs +12 -7
  45. package/dist/extension/tools/attachments_read.mjs +17 -4
  46. package/dist/extension/tools/attachments_table.d.ts +4 -4
  47. package/dist/extension/tools/questions_ask.d.ts +3 -1
  48. package/dist/extension/tools/questions_ask.mjs +2 -0
  49. package/package.json +3 -3
@@ -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 { p as reportTelegramError } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
7
+ import { p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
8
8
  import { describableImage } from "./images.mjs";
9
9
  import { MAXIMUM_ROUTING_PDF_CHARACTERS, extractRoutingPdf } from "./pdf-text.mjs";
10
10
  import { gateway, generateText } from "ai";
@@ -6,11 +6,11 @@ __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { appOrigin, channelsEnabled, proxyEnvironment } from "../environment.mjs";
8
8
  import "./principal.mjs";
9
- import { confirmationLocale } from "./confirmation-text.mjs";
10
9
  import { redactDiagnostic } from "../diagnostics.mjs";
11
- import { runtimeToken } from "../runtime-token.mjs";
10
+ import { confirmationLocale } from "./confirmation-text.mjs";
12
11
  import { inTelegramSendQueue, telegramSendScope } from "./send-queue.mjs";
13
- import { c as customerCopy, f as formattingRefused, l as customerMessage, p as reportTelegramError } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
12
+ import { c as customerCopy, f as formattingRefused, l as customerMessage, p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
13
+ import { runtimeToken } from "../runtime-token.mjs";
14
14
  import { MAXIMUM_PROXY_BODY_BYTES, connectionProxy } from "../connection-proxy.mjs";
15
15
  import { ACT_AS_HEADER } from "../ontology-connection.mjs";
16
16
  import { AttachmentError, CONVERSATION_HEADER, SESSION_HEADER } from "../attachments.mjs";
@@ -5,11 +5,11 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { proxyEnvironment } from "../environment.mjs";
8
+ import { telegramSendScope } from "./send-queue.mjs";
9
+ import { m as safeTelegramFetch, p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
8
10
  import { runtimeToken } from "../runtime-token.mjs";
9
11
  import { deferredConfirmations } from "../deferred-confirmations.mjs";
10
12
  import { conversations } from "../conversations.mjs";
11
- import { telegramSendScope } from "./send-queue.mjs";
12
- import { m as safeTelegramFetch, p as reportTelegramError } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
13
13
  import { expireDeferredRecord } from "./deferred-confirmations.mjs";
14
14
  import { connectionProxy } from "../connection-proxy.mjs";
15
15
  import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
@@ -52,7 +52,7 @@ function confirmationCopy(value) {
52
52
  function renderConfirmation(description, copy) {
53
53
  copy = confirmationCopy(copy);
54
54
  const action = humanText(description?.action);
55
- if (description !== void 0 && (action === void 0 || action === "❓")) throw new Error("A confirmation requires a safe declared action title.");
55
+ if (description === void 0 || action === void 0 || action === "❓") throw new Error("A confirmation requires a safe declared action title.");
56
56
  const facts = [[description?.warning ? "⚠️" : "", action].filter(Boolean).join(" "), ...(description?.fields ?? []).map(({ label, value }) => {
57
57
  if (value.length > 3584) throw new Error("The complete confirmation facts cannot fit in one Telegram message.");
58
58
  const safeLabel = humanText(label);
@@ -7,11 +7,11 @@ __eveCreateRequire(import.meta.url);
7
7
  import { memberIdOf, telegramPrincipal } from "./principal.mjs";
8
8
  import { humanText } from "../confirmation-description.mjs";
9
9
  import { renderConfirmation, withConfirmationText } from "./confirmation-text.mjs";
10
- import { chatPostOperationId, isChatPost } from "../chat-post-confirmation.mjs";
11
10
  import { chatPostCard, chatPostCardStatus, chatPostReceipt, withChatPostCard } from "./chat-post-card.mjs";
11
+ import { n as currentInboundRequest, p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
12
+ import { chatPostOperationId, isChatPost } from "../chat-post-confirmation.mjs";
12
13
  import { confirmations } from "../confirmations.mjs";
13
14
  import { conversationEvent } from "../conversations.mjs";
14
- import { n as currentInboundRequest, p as reportTelegramError } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
15
15
  import { formatAddress, scopedAddress } from "./sessions.mjs";
16
16
  import { agentMessage, observedChatKey, withConfirmationPrompt } from "./observed.mjs";
17
17
  import { postDeferredCard } from "./deferred-confirmations.mjs";
@@ -17,7 +17,7 @@ export declare function telegramRecord(value: unknown): Readonly<Record<string,
17
17
  export declare function messageContent(raw: Readonly<Record<string, unknown>>): MessageContent;
18
18
  /** Only shape/size diagnostics: do not retain/log raw webhook bodies, URLs or credential values. */
19
19
  export declare function contentShape(raw: Readonly<Record<string, unknown>>, content?: MessageContent): {
20
- contentSource: "none" | "text" | "caption" | "rich_message";
20
+ contentSource: "text" | "caption" | "none" | "rich_message";
21
21
  textType: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function";
22
22
  captionType: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function";
23
23
  hasRichMessage: boolean;
@@ -4,8 +4,8 @@ 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 { n as currentInboundRequest, s as currentThreadId } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
7
8
  import { conversationEvent } from "../conversations.mjs";
8
- import { n as currentInboundRequest, s as currentThreadId } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
9
9
  import { contentShape, messageContent } from "./content.mjs";
10
10
  import { currentConfirmationPrompt, messageMetadata, observedChatKey } from "./observed.mjs";
11
11
  import { archivedRoutingMessages } from "./routing-context.mjs";
@@ -1,7 +1,7 @@
1
1
  import { type TelegramPreviewContext } from "@segmentstream-cloud/ontology/telegram";
2
2
  import { type TelegramText } from "./format.js";
3
3
  export { isTechnicalText } from "../customer-text-safety.js";
4
- export declare function withDeliveryLocale<T>(locale: string, run: () => T, threadId?: string): T;
4
+ export declare function withDeliveryLocale<T>(locale: string, run: () => T, threadId?: string, replyTo?: string, chatId?: string): T;
5
5
  export declare function currentLocale(): string;
6
6
  export declare function currentThreadId(): string | undefined;
7
7
  export declare function customerCopy(): {
@@ -5,5 +5,5 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { isTechnicalText } from "../customer-text-safety.mjs";
8
- import { a as TelegramDeliveryError, c as customerCopy, d as failureText, f as formattingRefused, h as withDeliveryLocale, l as customerMessage, m as safeTelegramFetch, o as currentLocale, p as reportTelegramError, s as currentThreadId, u as customerText } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
8
+ import { a as TelegramDeliveryError, c as customerCopy, d as failureText, f as formattingRefused, h as withDeliveryLocale, l as customerMessage, m as safeTelegramFetch, o as currentLocale, p as reportTelegramError, s as currentThreadId, u as customerText } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
9
9
  export { TelegramDeliveryError, currentLocale, currentThreadId, customerCopy, customerMessage, customerText, failureText, formattingRefused, isTechnicalText, reportTelegramError, safeTelegramFetch, withDeliveryLocale };
@@ -7,12 +7,12 @@ __eveCreateRequire(import.meta.url);
7
7
  import { telegramPrincipal } from "./principal.mjs";
8
8
  import { humanText } from "../confirmation-description.mjs";
9
9
  import { renderConfirmation, withConfirmationText } from "./confirmation-text.mjs";
10
- import { confirmationOutcome } from "../confirmation-outcome.mjs";
11
10
  import { chatPostCard, chatPostCardStatus, chatPostReceipt, withChatPostCard } from "./chat-post-card.mjs";
11
+ import { n as currentInboundRequest, p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
12
12
  import { CONFIRMATION_TTL_MS, inputHash } from "../confirmations.mjs";
13
+ import { confirmationOutcome } from "../confirmation-outcome.mjs";
13
14
  import { deferredConfirmations, deferredKey, runDeferredAction } from "../deferred-confirmations.mjs";
14
15
  import { conversationEvent } from "../conversations.mjs";
15
- import { n as currentInboundRequest, p as reportTelegramError } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
16
16
  import { formatAddress } from "./sessions.mjs";
17
17
  import { agentMessage, observedChatKey, withConfirmationPrompt } from "./observed.mjs";
18
18
  import { telegramTextLinks } from "@segmentstream-cloud/ontology/telegram";
@@ -5,7 +5,7 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { conversationLanguage } from "./principal.mjs";
8
- import { d as failureText, p as reportTelegramError } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
8
+ import { d as failureText, p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
9
9
  import { describableImage, describeReceivedImage } from "./images.mjs";
10
10
  import { MAXIMUM_ATTACHMENT_BYTES, attachmentUrl } from "../attachments.mjs";
11
11
  function telegramConversation(chatId) {
@@ -4,6 +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 { t as agentBuild } from "../../../_chunks/build-metadata-DMTUHsRx.mjs";
7
8
  import { runtimeRequestFailure } from "./runtime-request.mjs";
8
9
  import { GET } from "eve/channels";
9
10
  const HEALTH_ROUTE = "/eve/v1/telegram/health";
@@ -14,7 +15,8 @@ function healthRoute(input) {
14
15
  const registry = input.chats.health();
15
16
  return Response.json({
16
17
  status: registry.degraded ? "degraded" : "ok",
17
- registry
18
+ registry,
19
+ runtime: agentBuild()
18
20
  }, {
19
21
  status: registry.degraded ? 503 : 200,
20
22
  headers: { "cache-control": "no-store" }
@@ -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 { p as reportTelegramError } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
7
+ import { p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
8
8
  import { gateway, generateText } from "ai";
9
9
  const MAXIMUM_IMAGE_DESCRIPTION_BYTES = 5242880;
10
10
  const MAXIMUM_NATIVE_IMAGE_BYTES = 3145728;
@@ -29,7 +29,16 @@ export declare class InboundRequest {
29
29
  /** The agent's description from the compiled manifest, for the routing decision. */
30
30
  readonly agentDescription: () => Promise<string>;
31
31
  readonly agentName: () => Promise<string>;
32
+ readonly traceId: `${string}-${string}-${string}-${string}-${string}`;
32
33
  edited: boolean;
34
+ triggeringMessageId: string | undefined;
35
+ chatId: string | undefined;
36
+ replyTo: string | undefined;
37
+ stopTyping: (() => void) | undefined;
38
+ lifecycle: ReturnType<typeof import("./lifecycle.js").attachmentLifecycle> | undefined;
39
+ lifecycleKey: string | undefined;
40
+ lifecycleContext: string | undefined;
41
+ monitor: ((session: Awaited<ReturnType<ChannelSource<unknown>["send"]>>, marker: string) => void) | undefined;
33
42
  previousText: string | undefined;
34
43
  attachmentsChanged: boolean;
35
44
  /** Evidence may be reused only while the provider attachment identities are unchanged. */
@@ -58,6 +67,8 @@ export declare class InboundRequest {
58
67
  extraFiles: Exclude<UserContent, string> | undefined;
59
68
  /** The route's session operations, on addresses already mapped to the configured scope. */
60
69
  sessions: SessionOperations | undefined;
70
+ /** Host background lifetime; long-running observers must never enter the routing drain. */
71
+ backgroundUntil: ((task: Promise<unknown>) => void) | undefined;
61
72
  /** Keeps work the request started, such as a chat report, alive after it answers. */
62
73
  waitUntil: ((task: Promise<unknown>) => void) | undefined;
63
74
  /** Continues the exact pending question's session, including under thread scope. */
@@ -4,5 +4,5 @@ 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 { i as withReplacedText, n as currentInboundRequest, r as runInboundRequest, t as InboundRequest } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
7
+ import { i as withReplacedText, n as currentInboundRequest, r as runInboundRequest, t as InboundRequest } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
8
8
  export { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText };
@@ -1,3 +1,4 @@
1
+ import { attachmentLifecycle } from "./lifecycle.js";
1
2
  import type { TelegramContext, TelegramInboundResult, TelegramMessage } from "eve/channels/telegram";
2
3
  import type { AdmissionSettings, SessionSettings, TelegramPolicy } from "../../config.js";
3
4
  import type { AgentAttachments } from "../attachments.js";
@@ -38,6 +39,7 @@ import type { PdfTextExtractor } from "./pdf-text.js";
38
39
  */
39
40
  export interface InboundDependencies {
40
41
  readonly policy: TelegramPolicy;
42
+ readonly lifecycle?: (principal: import("eve/context").SessionAuthContext, memberId: string, message: TelegramMessage) => ReturnType<typeof attachmentLifecycle>;
41
43
  readonly admission: () => AdmissionSettings;
42
44
  readonly sessions: () => SessionSettings;
43
45
  readonly sessionPolicy: SessionPolicy;
@@ -5,7 +5,8 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { conversationLanguage, telegramPrincipal } from "./principal.mjs";
8
- import { p as reportTelegramError } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
8
+ import { p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
9
+ import { startTypingHeartbeat, typingUntilComplete } from "./lifecycle.mjs";
9
10
  import { formatAddress, messageAddress, scopedAddress } from "./sessions.mjs";
10
11
  import { contentShape, messageContent } from "./content.mjs";
11
12
  import { messageAudience, otherRecipient } from "./audience.mjs";
@@ -363,6 +364,65 @@ async function handleInboundMessage(input) {
363
364
  turnAdmitted = true;
364
365
  return true;
365
366
  }
367
+ if (input.request !== void 0 && sent.attachments.length > 0 && dependencies.lifecycle !== void 0 && await admitTurn(true)) {
368
+ const principal = telegramPrincipal({
369
+ message: sent,
370
+ member
371
+ });
372
+ input.request.lifecycle = dependencies.lifecycle(principal, member.id, sent);
373
+ input.request.stopTyping = startTypingHeartbeat(context.telegram);
374
+ try {
375
+ const admitted = await input.request.lifecycle("admitted", { attachments: sent.attachments.map((file) => ({
376
+ id: file.fileId,
377
+ filename: file.fileName ?? file.kind,
378
+ mediaType: file.mediaType,
379
+ kind: file.kind
380
+ })) });
381
+ input.request.lifecycleKey = admitted.key;
382
+ input.request.replyTo = admitted.replyTo ?? sent.messageId;
383
+ if (admitted.key === void 0) input.request.lifecycle = void 0;
384
+ if (admitted.acknowledgement) try {
385
+ await context.telegram.post({
386
+ text: admitted.acknowledgement,
387
+ reply_parameters: { message_id: Number(input.request.replyTo) }
388
+ });
389
+ console.info("[segmentstream] telegram.phase", {
390
+ phase: "acknowledged",
391
+ at: new Date().toISOString(),
392
+ traceId: input.request.traceId,
393
+ intakeId: admitted.key,
394
+ chatId: sent.chat.id,
395
+ messageId: sent.messageId
396
+ });
397
+ } catch (error) {
398
+ reportTelegramError("intake_acknowledgement_failed", error, {
399
+ chatId: sent.chat.id,
400
+ messageId: sent.messageId
401
+ });
402
+ }
403
+ } catch (error) {
404
+ reportTelegramError("intake_admission_failed", error, {
405
+ chatId: sent.chat.id,
406
+ messageId: sent.messageId
407
+ });
408
+ input.request.lifecycle = void 0;
409
+ }
410
+ }
411
+ if (input.request !== void 0) {
412
+ input.request.triggeringMessageId = sent.messageId;
413
+ input.request.chatId = sent.chat.id;
414
+ input.request.replyTo ??= sent.messageId;
415
+ input.request.monitor = (session, marker) => {
416
+ input.request?.backgroundUntil?.(typingUntilComplete(session, marker, context.telegram, AbortSignal.timeout(6e5), {
417
+ traceId: input.request?.traceId,
418
+ intakeId: input.request?.lifecycleKey,
419
+ chatId: sent.chat.id,
420
+ messageId: sent.messageId
421
+ }).catch((error) => {
422
+ reportTelegramError("typing_monitor_failed", error, { sessionId: session.id });
423
+ }));
424
+ };
425
+ }
366
426
  let address = scopedAddress(messageAddress(sent), dependencies.sessions().scope);
367
427
  const operations = input.request?.sessions;
368
428
  const topicAddress = formatAddress({
@@ -393,7 +453,7 @@ async function handleInboundMessage(input) {
393
453
  address = pending.address || (questionSession === void 0 ? topicAddress : memberQuestionAddress);
394
454
  input.request.address = address;
395
455
  }
396
- if (group && (addressed === void 0 && !answering && dependencies.policy === "mention" || elsewhere !== void 0 && (addressed === void 0 || addressed === "reply-to-bot" && !recipientAnswer))) {
456
+ if (input.request?.lifecycleKey === void 0 && group && (addressed === void 0 && !answering && dependencies.policy === "mention" || elsewhere !== void 0 && (addressed === void 0 || addressed === "reply-to-bot" && !recipientAnswer))) {
397
457
  await recordInboundContent({
398
458
  text,
399
459
  decoded
@@ -481,7 +541,7 @@ async function handleInboundMessage(input) {
481
541
  };
482
542
  return question;
483
543
  };
484
- const route = await routeMessage({
544
+ const route = input.request?.lifecycleKey !== void 0 ? "reply" : await routeMessage({
485
545
  chatId: sent.chat.id,
486
546
  memberId: member.id,
487
547
  policy: dependencies.policy,
@@ -539,6 +599,10 @@ async function handleInboundMessage(input) {
539
599
  return conversation();
540
600
  }
541
601
  });
602
+ if (input.request?.lifecycleKey !== void 0) recordRouteDecision("reply", addressed, dependencies.policy, {
603
+ reason: "workspace_attachment_lifecycle",
604
+ note: "The configured workspace action admitted this attachment task."
605
+ });
542
606
  if (admissionRefused) {
543
607
  await recordInboundContent({
544
608
  text,
@@ -564,6 +628,66 @@ async function handleInboundMessage(input) {
564
628
  } : mediaPlan },
565
629
  ...dependencies.describeImage === void 0 ? {} : { describeImage: dependencies.describeImage }
566
630
  });
631
+ if (input.request?.lifecycle !== void 0) {
632
+ const lifecycle = input.request.lifecycle;
633
+ const key = input.request.lifecycleKey;
634
+ for (const [sourceFile, file] of files.entries()) {
635
+ if (file.status === "refused") {
636
+ await lifecycle("stored", {
637
+ key,
638
+ sourceFile,
639
+ error: {
640
+ filename: file.filename,
641
+ reason: file.reason
642
+ }
643
+ });
644
+ continue;
645
+ }
646
+ const stored = await lifecycle("stored", {
647
+ key,
648
+ sourceFile,
649
+ attachment: {
650
+ id: file.attachment.id,
651
+ filename: file.attachment.filename,
652
+ sha256: file.attachment.sha256
653
+ }
654
+ });
655
+ if (stored.retain !== void 0 && dependencies.attachments !== void 0) {
656
+ let outcome;
657
+ try {
658
+ outcome = { result: (await dependencies.attachments.promote({
659
+ memberId: member.id,
660
+ conversation: `telegram:${sent.chat.id}`
661
+ }, file.attachment.id, stored.retain)).result };
662
+ } catch (error) {
663
+ outcome = { error: reportTelegramError("attachment_retention_failed", error, { attachmentId: file.attachment.id }) };
664
+ }
665
+ try {
666
+ await lifecycle("retained", {
667
+ key,
668
+ attachment: file.attachment.id,
669
+ ...outcome
670
+ });
671
+ } catch (error) {
672
+ reportTelegramError("attachment_receipt_recording_failed", error, { attachmentId: file.attachment.id });
673
+ await lifecycle("retained", {
674
+ key,
675
+ attachment: file.attachment.id,
676
+ ...outcome
677
+ });
678
+ }
679
+ }
680
+ }
681
+ let current = await lifecycle("ready", { key });
682
+ const collectionTimeoutAt = Date.now() + 12e4;
683
+ while (current.readyAt !== void 0 && current.readyAt > Date.now()) {
684
+ if (Date.now() >= collectionTimeoutAt) throw new Error("The attachment collection exceeded its processing budget.");
685
+ await new Promise((resolve) => setTimeout(resolve, Math.min(2e4, current.readyAt - Date.now(), collectionTimeoutAt - Date.now())));
686
+ current = await lifecycle("ready", { key });
687
+ }
688
+ input.request.lifecycleContext = current.context;
689
+ input.request.replyTo = current.replyTo ?? input.request.replyTo;
690
+ }
567
691
  if (routingFile !== void 0 && processedMedia !== void 0) {
568
692
  const stored = files.get(routingFile.fileId);
569
693
  if (stored?.status === "stored" && stored.imageInput === "text") files = new Map(files).set(routingFile.fileId, {
@@ -599,6 +723,8 @@ async function handleInboundMessage(input) {
599
723
  if (voice !== void 0 && input.request !== void 0) input.request.text = text;
600
724
  else if (voice !== void 0) notes.push(text);
601
725
  if (answering && pending !== void 0 && input.request !== void 0) {
726
+ input.request.replyTo = pending.replyToMessageId ?? input.request.replyTo;
727
+ input.request.lifecycleKey = pending.taskId ?? input.request.lifecycleKey;
602
728
  input.request.text = `${answerMarker(pending.id)}\n${text}`;
603
729
  notes.push(`<pending_question>\nTask: ${pending.task}\nQuestion: ${pending.question}\nUse this member's answer to continue the pending task with their current permissions.
604
730
  </pending_question>`);
@@ -667,10 +793,26 @@ async function handleInboundMessage(input) {
667
793
  ...principal,
668
794
  attributes: {
669
795
  ...principal.attributes,
796
+ replyToMessageId: input.request?.replyTo ?? sent.messageId,
797
+ ...input.request?.lifecycleKey === void 0 ? {} : {
798
+ taskId: input.request.lifecycleKey,
799
+ triggeringMessageId: sent.messageId
800
+ },
670
801
  pendingQuestionId: pending.id
671
802
  }
672
- } : principal,
803
+ } : {
804
+ ...principal,
805
+ attributes: {
806
+ ...principal.attributes,
807
+ replyToMessageId: input.request?.replyTo ?? sent.messageId,
808
+ ...input.request?.lifecycleKey === void 0 ? {} : {
809
+ taskId: input.request.lifecycleKey,
810
+ triggeringMessageId: sent.messageId
811
+ }
812
+ }
813
+ },
673
814
  context: [
815
+ ...input.request?.lifecycleContext === void 0 ? [] : [input.request.lifecycleContext],
674
816
  ...observedContext(dependencies.observed.take(chat), sharedContext),
675
817
  ...group ? chatContext(dependencies.chats.chat(sent.chat.id)) : [],
676
818
  speakerBlock({
@@ -0,0 +1,46 @@
1
+ import type { SessionAuthContext } from "eve/context";
2
+ import type { TelegramContext } from "eve/channels/telegram";
3
+ import type { Session } from "eve/channels";
4
+ import type { Environment } from "../environment.js";
5
+ import type { RuntimeToken } from "../runtime-token.js";
6
+ export interface AttachmentLifecycle {
7
+ readonly entity: string;
8
+ readonly action: string;
9
+ }
10
+ export interface LifecycleResult {
11
+ readonly key?: string;
12
+ readonly replyTo?: string;
13
+ readonly acknowledgement?: string;
14
+ readonly readyAt?: number;
15
+ readonly context?: string;
16
+ readonly complete?: boolean;
17
+ readonly message?: string;
18
+ readonly retain?: {
19
+ readonly entity: string;
20
+ readonly action: string;
21
+ readonly input: Record<string, unknown>;
22
+ };
23
+ }
24
+ /** Opt-in workspace action; the workspace owns grouping, copy, retention and completion decisions. */
25
+ export declare function attachmentLifecycle(options: {
26
+ readonly configuration: AttachmentLifecycle;
27
+ readonly principal: SessionAuthContext;
28
+ readonly memberId: string;
29
+ readonly message: {
30
+ readonly chat: {
31
+ readonly id: string;
32
+ };
33
+ readonly messageId: string;
34
+ readonly messageThreadId?: number;
35
+ readonly from?: {
36
+ readonly id: string;
37
+ };
38
+ };
39
+ readonly environment?: Environment;
40
+ readonly token?: RuntimeToken;
41
+ readonly fetch?: typeof fetch;
42
+ }): (phase: "admitted" | "stored" | "retained" | "ready" | "completed", data?: Record<string, unknown>) => Promise<LifecycleResult>;
43
+ /** Kept inside the webhook's waitUntil lifetime; terminal durable stream events close the reader. */
44
+ export declare function typingUntilComplete(session: Session, marker: string, telegram: TelegramContext["telegram"], signal: AbortSignal, metadata?: Readonly<Record<string, string | undefined>>): Promise<void>;
45
+ /** No global timer survives a request or terminal event; failures retain safe method/status telemetry. */
46
+ export declare function startTypingHeartbeat(telegram: TelegramContext["telegram"]): () => void;
@@ -0,0 +1,163 @@
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 { t as agentBuild } from "../../../_chunks/build-metadata-DMTUHsRx.mjs";
8
+ import { object } from "../confirmation-description.mjs";
9
+ import { n as currentInboundRequest, p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
10
+ import { needsConfirmation } from "../confirmations.mjs";
11
+ import { actionToolName, ontologyTools } from "../ontology-tools.mjs";
12
+ function attachmentLifecycle(options) {
13
+ const tools = ontologyTools(options);
14
+ const action = actionToolName(options.configuration.entity, options.configuration.action);
15
+ return async (phase, data = {}) => {
16
+ console.info("[segmentstream] telegram.phase", {
17
+ phase,
18
+ traceId: currentInboundRequest()?.traceId,
19
+ at: new Date().toISOString(),
20
+ chatId: options.message.chat.id,
21
+ messageId: options.message.messageId,
22
+ memberId: options.memberId,
23
+ intakeId: data.key,
24
+ runtime: agentBuild(options.environment)
25
+ });
26
+ const input = {
27
+ phase,
28
+ source: {
29
+ chat: options.message.chat.id,
30
+ topic: String(options.message.messageThreadId ?? ""),
31
+ message: options.message.messageId,
32
+ sender: options.message.from.id,
33
+ locale: String(options.principal.attributes.conversationLanguage ?? options.principal.attributes.locale ?? "en")
34
+ },
35
+ data
36
+ };
37
+ if (needsConfirmation(await tools.labels(options.memberId, action, input))) throw new Error("An attachment lifecycle hook must be a permitted routine action.");
38
+ const raw = await tools.rpc(options.memberId, "tools/call", {
39
+ name: action,
40
+ arguments: input
41
+ });
42
+ const envelope = object(raw.structuredContent);
43
+ if (object(envelope.validation).ok === false) throw new Error("The attachment lifecycle action rejected its event.");
44
+ const result = object(envelope.result ?? envelope);
45
+ const retention = object(result.retain);
46
+ if (typeof retention.entity === "string" && typeof retention.action === "string" && needsConfirmation(await tools.labels(options.memberId, actionToolName(retention.entity, retention.action), object(retention.input)))) throw new Error("Automatic retention requires a permitted routine storage action.");
47
+ return {
48
+ ...typeof result.key === "string" ? { key: result.key } : {},
49
+ ...typeof result.reply_to === "string" && /^[1-9]\d*$/.test(result.reply_to) ? { replyTo: result.reply_to } : {},
50
+ ...typeof result.acknowledgement === "string" ? { acknowledgement: result.acknowledgement } : {},
51
+ ...typeof result.ready_at === "number" && Number.isSafeInteger(result.ready_at) ? { readyAt: result.ready_at } : {},
52
+ ...typeof result.context === "string" ? { context: result.context } : {},
53
+ ...typeof result.complete === "boolean" ? { complete: result.complete } : {},
54
+ ...typeof result.message === "string" ? { message: result.message } : {},
55
+ ...typeof object(result.retain).entity === "string" && typeof object(result.retain).action === "string" ? { retain: {
56
+ entity: String(object(result.retain).entity),
57
+ action: String(object(result.retain).action),
58
+ input: object(object(result.retain).input)
59
+ } } : {}
60
+ };
61
+ };
62
+ }
63
+ async function typingUntilComplete(session, marker, telegram, signal, metadata = {}) {
64
+ const reader = (await session.getEventStream({ follow: true })).getReader();
65
+ let turn;
66
+ let firstResponse = false;
67
+ const abort = () => {
68
+ reader.cancel().catch(() => void 0);
69
+ };
70
+ signal.addEventListener("abort", abort, { once: true });
71
+ const stop = startTypingHeartbeat(telegram);
72
+ try {
73
+ while (!signal.aborted) {
74
+ const item = await reader.read();
75
+ if (item.done) break;
76
+ const event = item.value;
77
+ if (event.type === "message.received" && event.data.message.includes(marker)) {
78
+ turn = event.data.turnId;
79
+ console.info("[segmentstream] telegram.phase", {
80
+ ...metadata,
81
+ phase: "turn_received",
82
+ at: new Date().toISOString(),
83
+ sessionId: session.id,
84
+ turnId: turn
85
+ });
86
+ }
87
+ if (turn && event.data !== void 0 && "turnId" in event.data && event.data.turnId === turn && [
88
+ "step.started",
89
+ "actions.requested",
90
+ "action.result",
91
+ "turn.completed",
92
+ "turn.failed",
93
+ "turn.cancelled"
94
+ ].includes(event.type)) console.info("[segmentstream] telegram.phase", {
95
+ ...metadata,
96
+ phase: event.type,
97
+ at: new Date().toISOString(),
98
+ sessionId: session.id,
99
+ turnId: turn
100
+ });
101
+ if (turn && !firstResponse && (event.type === "message.appended" || event.type === "reasoning.appended") && "turnId" in event.data && event.data.turnId === turn) {
102
+ firstResponse = true;
103
+ console.info("[segmentstream] telegram.phase", {
104
+ ...metadata,
105
+ phase: "model_first_response",
106
+ at: new Date().toISOString(),
107
+ sessionId: session.id,
108
+ turnId: turn
109
+ });
110
+ }
111
+ if (event.type === "session.failed" || event.type === "session.completed") break;
112
+ if (turn && "turnId" in event.data && event.data.turnId === turn && ([
113
+ "turn.completed",
114
+ "turn.failed",
115
+ "turn.cancelled",
116
+ "input.requested"
117
+ ].includes(event.type) || event.type === "turn.waiting" && event.data.on === "input")) break;
118
+ }
119
+ } finally {
120
+ stop();
121
+ signal.removeEventListener("abort", abort);
122
+ await reader.cancel().catch(() => void 0);
123
+ reader.releaseLock();
124
+ }
125
+ }
126
+ function startTypingHeartbeat(telegram) {
127
+ let stopped = false;
128
+ let timer;
129
+ const tick = async () => {
130
+ try {
131
+ const response = await telegram.request("sendChatAction", {
132
+ chat_id: telegram.chatId,
133
+ action: "typing",
134
+ ...telegram.messageThreadId === void 0 ? {} : { message_thread_id: telegram.messageThreadId }
135
+ });
136
+ console.info("[segmentstream] telegram.method", {
137
+ method: "sendChatAction",
138
+ at: new Date().toISOString(),
139
+ chatId: telegram.chatId,
140
+ status: response.status,
141
+ accepted: response.ok && object(response.body).ok !== false
142
+ });
143
+ if (!response.ok || object(response.body).ok === false) throw new Error("Telegram rejected sendChatAction.", { cause: {
144
+ status: response.status,
145
+ body: response.body
146
+ } });
147
+ } catch (error) {
148
+ reportTelegramError("typing_failed", error, {
149
+ method: "sendChatAction",
150
+ chatId: telegram.chatId
151
+ });
152
+ }
153
+ if (!stopped) timer = setTimeout(() => {
154
+ tick();
155
+ }, 4e3);
156
+ };
157
+ tick();
158
+ return () => {
159
+ stopped = true;
160
+ if (timer !== void 0) clearTimeout(timer);
161
+ };
162
+ }
163
+ export { attachmentLifecycle, startTypingHeartbeat, typingUntilComplete };
@@ -5,7 +5,7 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { DEFAULT_IMAGE_DESCRIPTION_MODEL } from "../../config.mjs";
8
- import { p as reportTelegramError } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
8
+ import { p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
9
9
  function planMedia(capabilities, descriptionModel = null) {
10
10
  if (typeof descriptionModel === "string") return {
11
11
  images: "describe",
@@ -8,7 +8,7 @@ import "./principal.mjs";
8
8
  import { confirmationLocale } from "./confirmation-text.mjs";
9
9
  import { splitTelegramText } from "./format.mjs";
10
10
  import { inTelegramSendQueue } from "./send-queue.mjs";
11
- import { a as TelegramDeliveryError, l as customerMessage, o as currentLocale, p as reportTelegramError } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
11
+ import { a as TelegramDeliveryError, l as customerMessage, o as currentLocale, p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
12
12
  import { agentMessage, observedChatKey } from "./observed.mjs";
13
13
  const SILENT_MARKERS = new Set(["[silent]", "no_reply"]);
14
14
  function isSilentReply(text) {