@segmentstream/agent 0.26.1 → 0.28.0

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 (62) hide show
  1. package/README.md +41 -43
  2. package/dist/_chunks/{build-metadata-D_Gqlb5f.mjs → build-metadata-CMmCRD8l.mjs} +1 -1
  3. package/dist/extension/config.d.ts +11 -12
  4. package/dist/extension/config.mjs +20 -37
  5. package/dist/extension/extension.d.ts +2 -2
  6. package/dist/extension/extension.mjs +3 -3
  7. package/dist/extension/hooks/chat-context.d.ts +3 -0
  8. package/dist/extension/hooks/chat-context.mjs +10 -0
  9. package/dist/extension/hooks/sandbox.d.ts +2 -3
  10. package/dist/extension/instructions/conversations.d.ts +3 -2
  11. package/dist/extension/instructions/conversations.mjs +7 -11
  12. package/dist/extension/instructions.md +18 -0
  13. package/dist/extension/lib/build-metadata.mjs +1 -1
  14. package/dist/extension/lib/chat-context.d.ts +108 -0
  15. package/dist/extension/lib/chat-context.mjs +217 -0
  16. package/dist/extension/lib/conversation-runtime.d.ts +1 -1
  17. package/dist/extension/lib/conversation-runtime.mjs +1 -23
  18. package/dist/extension/lib/conversations.d.ts +10 -6
  19. package/dist/extension/lib/conversations.mjs +28 -42
  20. package/dist/extension/lib/group-routing/addressing.d.ts +18 -0
  21. package/dist/extension/lib/group-routing/addressing.mjs +108 -0
  22. package/dist/extension/lib/group-routing/burst.d.ts +1 -1
  23. package/dist/extension/lib/group-routing/burst.mjs +1 -1
  24. package/dist/extension/lib/group-routing/conversation-prompt.d.ts +5 -2
  25. package/dist/extension/lib/group-routing/conversation-prompt.mjs +26 -6
  26. package/dist/extension/lib/group-routing/conversation-router.d.ts +8 -16
  27. package/dist/extension/lib/group-routing/conversation-router.mjs +49 -93
  28. package/dist/extension/lib/group-routing/dequeue.mjs +1 -1
  29. package/dist/extension/lib/group-routing/engagement.d.ts +1 -1
  30. package/dist/extension/lib/group-routing/{observed.d.ts → messages.d.ts} +9 -33
  31. package/dist/extension/lib/group-routing/messages.mjs +38 -0
  32. package/dist/extension/lib/group-routing/policy.d.ts +13 -9
  33. package/dist/extension/lib/group-routing/policy.mjs +35 -25
  34. package/dist/extension/lib/group-routing/router.d.ts +3 -11
  35. package/dist/extension/lib/group-routing/router.mjs +11 -35
  36. package/dist/extension/lib/group-routing/routing-context.d.ts +6 -2
  37. package/dist/extension/lib/group-routing/routing-context.mjs +12 -11
  38. package/dist/extension/lib/telegram/adapter.d.ts +1 -3
  39. package/dist/extension/lib/telegram/adapter.mjs +9 -18
  40. package/dist/extension/lib/telegram/confirmations.d.ts +3 -3
  41. package/dist/extension/lib/telegram/confirmations.mjs +3 -25
  42. package/dist/extension/lib/telegram/conversations.d.ts +14 -3
  43. package/dist/extension/lib/telegram/conversations.mjs +16 -37
  44. package/dist/extension/lib/telegram/delivery-diagnostics.d.ts +4 -1
  45. package/dist/extension/lib/telegram/delivery-diagnostics.mjs +9 -3
  46. package/dist/extension/lib/telegram/health.mjs +1 -1
  47. package/dist/extension/lib/telegram/inbound.d.ts +14 -14
  48. package/dist/extension/lib/telegram/inbound.mjs +73 -78
  49. package/dist/extension/lib/telegram/lifecycle.mjs +1 -1
  50. package/dist/extension/lib/telegram/{observed.d.ts → message-metadata.d.ts} +2 -6
  51. package/dist/extension/lib/telegram/{observed.mjs → message-metadata.mjs} +1 -14
  52. package/dist/extension/lib/telegram/routing-context.d.ts +4 -3
  53. package/dist/extension/lib/telegram/routing-context.mjs +2 -2
  54. package/dist/extension/lib/telegram/sessions.d.ts +5 -52
  55. package/dist/extension/lib/telegram/sessions.mjs +1 -188
  56. package/dist/extension/lib/telegram/signals.mjs +1 -1
  57. package/dist/extension/lib/telegram/silent.d.ts +5 -6
  58. package/dist/extension/lib/telegram/silent.mjs +4 -31
  59. package/package.json +1 -1
  60. package/dist/extension/lib/group-routing/conversation.d.ts +0 -46
  61. package/dist/extension/lib/group-routing/conversation.mjs +0 -269
  62. package/dist/extension/lib/group-routing/observed.mjs +0 -103
@@ -6,8 +6,8 @@ __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { DEFAULT_GROUP_POLICY, groupPolicy, overlaySmartPolicy } from "./policy.mjs";
8
8
  import { addressingReason, otherRecipient, signalAudience } from "./signals.mjs";
9
- import { conversationState, conversationThreshold } from "./conversation.mjs";
10
- import { conversationDecisionRouter } from "./conversation-router.mjs";
9
+ import { conversationAddressing } from "./addressing.mjs";
10
+ import { addressedToAgent, conversationDecisionRouter } from "./conversation-router.mjs";
11
11
  import { recordRoutingMetric } from "./health.mjs";
12
12
  import { humanAddressee, humanThread, questionInvitation, restoreEngagement } from "./engagement.mjs";
13
13
  import { boundedRouterPayload, policyHash } from "./record.mjs";
@@ -76,9 +76,8 @@ function recentlyAddressed(message, sender) {
76
76
  function lastOutbound(question) {
77
77
  return question.recent.findLast((message) => isAgentMessage(message) && message.messageId !== void 0 && Date.parse(message.at) <= Date.parse(question.message.at));
78
78
  }
79
- function conversationContext(question, options = {}) {
79
+ function conversationContext(question) {
80
80
  const sentAt = Date.parse(question.message.at);
81
- const since = options.since;
82
81
  const policy = question.groups ?? DEFAULT_GROUP_POLICY;
83
82
  const recent = question.recent.slice(-policy.smart.context.messages).filter((message) => {
84
83
  if (!isAgentMessage(message)) return true;
@@ -140,8 +139,7 @@ function conversationContext(question, options = {}) {
140
139
  human_thread: humanThread(question.signals, question.recent, policy.smart.engagement.followUpMinutes)
141
140
  })
142
141
  },
143
- recent_messages: (since === void 0 ? recent : recent.filter((message) => Date.parse(message.at) >= since)).map((message) => line(message, bot, roster, question.message.at)),
144
- ...since === void 0 ? {} : { recent_messages_since: new Date(since).toISOString() },
142
+ recent_messages: recent.map((message) => line(message, bot, roster, question.message.at)),
145
143
  possible_follow_up_to: followUps.map((message) => line(message, bot, roster, question.message.at)),
146
144
  open_questions: openQuestions,
147
145
  last_outbound: JSON.stringify(outbound === void 0 ? null : {
@@ -302,25 +300,11 @@ async function routeMessage(input) {
302
300
  groups,
303
301
  signals
304
302
  };
305
- const analysis = conversationState({
306
- signals,
307
- recent: question.recent,
308
- policy: groups,
309
- agentNames: [...question.agentIdentity?.aliases ?? [], ...question.agentIdentity?.username ? [question.agentIdentity.username] : []],
310
- pendingQuestionAt: input.pendingQuestionAt,
311
- asOf: question.processingAt
312
- });
313
- question = {
314
- ...question,
315
- conversation: analysis
316
- };
303
+ const evidence = conversationAddressing(signals, question.recent, groups, [...question.agentIdentity?.aliases ?? [], ...question.agentIdentity?.username ? [question.agentIdentity.username] : []]);
317
304
  payload = {
318
305
  ...payload,
319
306
  burst_id: question.burstId,
320
- path: analysis.path,
321
- conversation: analysis.conversation,
322
- human_exchange: analysis.human_exchange,
323
- pending_question_voided: input.pendingQuestionAt !== void 0 && !analysis.pending_question_live,
307
+ addressing_evidence: evidence,
324
308
  question: {
325
309
  recent: question.recent,
326
310
  pendingQuestions: question.pendingQuestions,
@@ -330,30 +314,22 @@ async function routeMessage(input) {
330
314
  messagesToClassify: question.messagesToClassify
331
315
  }
332
316
  };
333
- if (analysis.path === "A") {
317
+ if (evidence.agent_named || evidence.reply_to_agent && evidence.human_targets.length === 0 && evidence.soft_target === void 0) {
334
318
  if (input.beforeDecide && !await input.beforeDecide(true)) return complete("observe", "admission_refused", "Admission refused before direct turn.");
335
319
  return complete("reply", addressed && addressed !== "pending-question" ? addressed : "mention", "Agent explicitly addressed.");
336
320
  }
337
- if (analysis.path === "H") return complete("observe", "human_addressee", "Message addresses another participant; conversation and question invitation closed.");
321
+ if (evidence.human_targets.length > 0) return complete("observe", "human_addressee", "Message replies to, tags or names another participant.");
338
322
  if (signals.addressing.eligible === false) return complete("observe", "channel_ineligible", "Channel excludes ambient messages.");
339
323
  if (groups.mode === "mention") return complete("observe", "mention_policy", "Mention policy observes unaddressed messages.");
340
324
  if (input.beforeDecide && !await input.beforeDecide()) return complete("observe", "admission_refused", "Admission refused before inference.");
341
325
  const result = await input.decide(question);
342
326
  const diagnostics = typeof result === "string" ? void 0 : result.diagnostics;
343
- const probabilities = diagnostics?.addressing;
344
- const threshold = conversationThreshold(analysis.path, groups);
345
- const valid = probabilities !== void 0 && probabilities.addressee.choice === "agent" && (probabilities.addressee.probabilities?.agent ?? NaN) >= threshold && (probabilities.needs_response.probabilities?.true ?? NaN) >= (analysis.path === "B" || analysis.path === "X" ? .7 : .5) && (probabilities.ends_conversation.probabilities?.true ?? NaN) < .7;
327
+ const valid = addressedToAgent(diagnostics?.addressing);
346
328
  payload = {
347
329
  ...payload,
348
- ...typeof result === "string" ? {} : result.router,
349
- thresholds_applied: {
350
- threshold,
351
- path: analysis.path,
352
- needs_response: analysis.path === "B" || analysis.path === "X" ? .7 : .5,
353
- ends_conversation: .7
354
- }
330
+ ...typeof result === "string" ? {} : result.router
355
331
  };
356
- return complete(valid ? "reply" : "observe", `decision_model_${valid ? "reply" : "observe"}`, "Conversation addressing probabilities evaluated.", "decision-model", diagnostics);
332
+ return complete(valid ? "reply" : "observe", `decision_model_${valid ? "reply" : "observe"}`, "Addressee read from the chat's recent messages.", "decision-model", diagnostics);
357
333
  } catch (error) {
358
334
  console.error("[segmentstream] conversation routing failed", { error });
359
335
  return complete("observe", error instanceof Error && error.name === "TimeoutError" ? "decision_model_timeout" : "decision_model_failed", "Conversation routing unavailable; observe fallback.");
@@ -1,4 +1,8 @@
1
- import { type RecentMessage } from "./observed.js";
1
+ import { type RecentMessage } from "./messages.js";
2
2
  import { type ConversationPage } from "../conversations.js";
3
- /** Cold ring: the same 30 messages, including unlinked humans and confirmed delivered bot messages. */
3
+ /**
4
+ * The archive's recent messages for routing, oldest first, including unlinked humans and confirmed
5
+ * delivered bot messages. A message that started a turn is `admitted`; stored native images of
6
+ * the others ride along by reference, for the next turn that answers.
7
+ */
4
8
  export declare function archivedRoutingMessages(events: ConversationPage["events"], limit?: number): ReadonlyArray<RecentMessage>;
@@ -5,7 +5,7 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { collapseConversationEvents } from "../conversations.mjs";
8
- import { agentMessage } from "./observed.mjs";
8
+ import { agentMessage } from "./messages.mjs";
9
9
  function archivedMetadata(event) {
10
10
  try {
11
11
  return JSON.parse(event.argsSummary ?? "null")?.routing ?? {};
@@ -34,15 +34,7 @@ function archivedRoutingMessages(events, limit = 30) {
34
34
  "thread-with-agent",
35
35
  "pending-question"
36
36
  ].includes(event.reason ?? "")).map((event) => [event.messageId, event.reason]));
37
- const ended = new Set(events.filter((event) => {
38
- if (event.kind !== "decision" || event.messageId === null) return false;
39
- try {
40
- const data = JSON.parse(event.argsSummary ?? "{}");
41
- return data.router?.conversation_ended === true || data.routing?.conversationEnded === true;
42
- } catch {
43
- return false;
44
- }
45
- }).map((event) => event.messageId));
37
+ const admitted = new Set(events.flatMap((event) => event.kind === "decision" && event.reason === "turn_admitted" && event.messageId !== null ? [event.messageId] : []));
46
38
  const membership = new Map();
47
39
  for (const event of [...events].reverse()) {
48
40
  if (event.kind !== "decision" || event.senderId === null) continue;
@@ -68,10 +60,19 @@ function archivedRoutingMessages(events, limit = 30) {
68
60
  } : recordedReply;
69
61
  const derivedText = metadata.decoded?.flatMap((item) => item.content === void 0 ? [] : [item.content]).join("\n");
70
62
  const text = (event.text || derivedText || (metadata.media ? "" : "[attachment or service message]")).slice(0, 2e3);
63
+ const images = (Array.isArray(metadata.files) ? metadata.files : []).filter((file) => file.image === true).map(({ id, filename, contentType, size, expiresAt }) => ({
64
+ id,
65
+ filename,
66
+ contentType,
67
+ size,
68
+ expiresAt
69
+ }));
71
70
  const common = {
72
71
  invitation: metadata.invitation ?? invitations.get(event.messageId ?? ""),
73
- ...ended.has(event.messageId) ? { conversationEnded: true } : {},
72
+ ...event.kind === "inbound" && admitted.has(event.messageId ?? "") ? { admitted: true } : {},
73
+ ...images.length === 0 ? {} : { images },
74
74
  author: event.senderName ?? event.senderId ?? "unknown sender",
75
+ ...typeof metadata.author === "string" && event.kind === "inbound" ? { signedAuthor: metadata.author } : {},
75
76
  text,
76
77
  ...metadata.media === void 0 ? {} : { media: metadata.media },
77
78
  at: event.at,
@@ -9,7 +9,6 @@ import { type RuntimeTokenVerifier } from "../runtime-identity.js";
9
9
  import { type RuntimeToken } from "../runtime-token.js";
10
10
  import { type ImageDescriber } from "./images.js";
11
11
  import { type CapabilityLookup } from "./media-policy.js";
12
- import { type ObservedMessages } from "./observed.js";
13
12
  import type { PdfTextExtractor } from "./pdf-text.js";
14
13
  import { type DecideFunction } from "./router.js";
15
14
  import { type HistoryReader } from "./sessions.js";
@@ -44,10 +43,9 @@ export interface TelegramAdapterOptions {
44
43
  readonly describeAttachment?: AttachmentDescriber;
45
44
  readonly extractPdf?: PdfTextExtractor;
46
45
  readonly modelCapabilities?: CapabilityLookup;
47
- readonly observed?: ObservedMessages;
48
46
  /** Verifies callers of the health route; tests pass one on a local key set. */
49
47
  readonly verify?: RuntimeTokenVerifier;
50
- /** Reads session events for routing, policy and interactions; tests pass a fake. */
48
+ /** Reads session events for routing and interactions; tests pass a fake. */
51
49
  readonly history?: HistoryReader;
52
50
  /** The attachment store; tests pass a fake. */
53
51
  readonly attachments?: AgentAttachments;
@@ -13,7 +13,7 @@ import { n as conversationLanguage, o as telegramPrincipal } from "../../../_chu
13
13
  import { object } from "../confirmation-description.mjs";
14
14
  import { confirmationLocale } from "./confirmation-text.mjs";
15
15
  import { telegramSendScope } from "./send-queue.mjs";
16
- import { conversations } from "../conversations.mjs";
16
+ import { conversations, topicContext } from "../conversations.mjs";
17
17
  import { withDeliveryDiagnostics } from "./delivery-diagnostics.mjs";
18
18
  import { d as failureText, h as withDeliveryLocale, i as withReplacedText, m as safeTelegramFetch, n as currentInboundRequest, o as currentLocale, p as reportTelegramError, r as runInboundRequest, t as InboundRequest } from "../../../_chunks/inbound-request-D1r0FkDI.mjs";
19
19
  import { chooseModel } from "../../model.mjs";
@@ -21,22 +21,21 @@ import { groupRoutingHealth } from "../group-routing/health.mjs";
21
21
  import { decisionRouter } from "../group-routing/router.mjs";
22
22
  import { archivedRoutingMessages } from "../group-routing/routing-context.mjs";
23
23
  import { dispatchStructuredMessage, structuredMessage } from "./structured.mjs";
24
- import { scopedAddress, sessionPolicy } from "./sessions.mjs";
24
+ import { scopedAddress } from "./sessions.mjs";
25
25
  import { handleWorkspaceCallback, isWorkspaceCallback } from "./callbacks.mjs";
26
- import { t as agentBuild } from "../../../_chunks/build-metadata-D_Gqlb5f.mjs";
26
+ import { t as agentBuild } from "../../../_chunks/build-metadata-CMmCRD8l.mjs";
27
27
  import { attachmentLifecycle } from "./lifecycle.mjs";
28
28
  import { gatewayImageDescriber } from "./images.mjs";
29
29
  import { gatewayAttachmentDescriber } from "./attachment-description.mjs";
30
30
  import { MAXIMUM_DOWNLOAD_BYTES, connectionProxy } from "../connection-proxy.mjs";
31
31
  import { agentAttachments, parseAttachmentUrl } from "../attachments.mjs";
32
32
  import { agentIdentities } from "../identities.mjs";
33
- import { observedMessages } from "./observed.mjs";
34
- import { interimTexts, replyDelivery } from "./silent.mjs";
35
- import { handleConfirmationCallback, telegramConfirmationEvents } from "./confirmations.mjs";
36
33
  import { captureRouteDecider, captureTelegramFetch, conversationIdentities, recordGateDecision, telegramConversationCapture } from "./conversations.mjs";
37
34
  import { botIdentityCache } from "./bot.mjs";
35
+ import { handleConfirmationCallback, telegramConfirmationEvents } from "./confirmations.mjs";
38
36
  import { gatewayMediaCapabilities, planMedia } from "./media-policy.mjs";
39
37
  import { agentChats } from "./chats.mjs";
38
+ import { interimTexts, replyDelivery } from "./silent.mjs";
40
39
  import { downloadTelegramFile, gatewayTranscriber } from "./voice.mjs";
41
40
  import { handleInboundMessage } from "./inbound.mjs";
42
41
  import { recoverableSessions } from "./session-recovery.mjs";
@@ -78,7 +77,6 @@ const eveInteractions = async (sessionId) => {
78
77
  function segmentstreamTelegram(options) {
79
78
  const environment = options.environment ?? process.env;
80
79
  const token = options.token ?? runtimeToken;
81
- const observed = options.observed ?? observedMessages(60);
82
80
  const readHistory = options.history ?? eveHistory;
83
81
  const readInteractions = options.history ?? eveInteractions;
84
82
  const capabilities = options.modelCapabilities ?? gatewayMediaCapabilities(options.fetch);
@@ -87,10 +85,6 @@ function segmentstreamTelegram(options) {
87
85
  token,
88
86
  fetch: options.fetch
89
87
  });
90
- const policy = sessionPolicy({
91
- settings: options.sessions,
92
- read: readHistory
93
- });
94
88
  const bot = botIdentityCache();
95
89
  const proxies = new Map();
96
90
  const interimDelivered = interimTexts();
@@ -127,7 +121,6 @@ function segmentstreamTelegram(options) {
127
121
  const capture = telegramConversationCapture({
128
122
  archive,
129
123
  identities: pairing,
130
- observed,
131
124
  chats,
132
125
  structured: (raw) => {
133
126
  const inbound = settings().inbound;
@@ -172,7 +165,7 @@ function segmentstreamTelegram(options) {
172
165
  archive,
173
166
  identities: pairing
174
167
  });
175
- const approvalEvents = telegramConfirmationEvents(archive, observed, () => options.sessions().scope);
168
+ const approvalEvents = telegramConfirmationEvents(archive, () => options.sessions().scope);
176
169
  const channel = telegramChannel({
177
170
  turnPolicy: "queue",
178
171
  api: {
@@ -203,10 +196,10 @@ function segmentstreamTelegram(options) {
203
196
  policy: settings().policy,
204
197
  decisionModel: options.models().decision,
205
198
  groups: options.groups?.(),
206
- recentConversation: async (chatId, threadId) => archivedRoutingMessages((await archive.context({
199
+ recentConversation: async (chatId, threadId) => archivedRoutingMessages((await topicContext(archive, {
207
200
  provider: "telegram",
208
201
  chatId,
209
- ...threadId === void 0 ? {} : { threadId },
202
+ threadId: threadId ?? null,
210
203
  limit: 200,
211
204
  includeDecisions: true
212
205
  })).events, 100),
@@ -236,7 +229,6 @@ function segmentstreamTelegram(options) {
236
229
  }) },
237
230
  sessions: options.sessions,
238
231
  admission: options.admission ?? (() => manifestOf({})),
239
- sessionPolicy: policy,
240
232
  sttModel: options.models().stt,
241
233
  attachmentsModel: options.models().imageDescription ?? options.models().attachments ?? "google/gemini-2.5-flash-lite",
242
234
  describeAttachment: options.describeAttachment ?? gatewayAttachmentDescriber,
@@ -285,7 +277,6 @@ function segmentstreamTelegram(options) {
285
277
  ...options
286
278
  }),
287
279
  attachments: attachments(),
288
- observed,
289
280
  readHistory,
290
281
  history: readInteractions,
291
282
  chats,
@@ -425,7 +416,7 @@ function segmentstreamTelegram(options) {
425
416
  await channel.telegram.post(failureText(reference));
426
417
  return;
427
418
  }
428
- await replyDelivery(observed, () => telegramSendScope(environment, settings().connection), {
419
+ await replyDelivery(() => telegramSendScope(environment, settings().connection), {
429
420
  enabled: () => settings().interimMessages === true,
430
421
  delivered: interimDelivered
431
422
  })(event, context);
@@ -1,13 +1,13 @@
1
1
  import type { ChannelEvents } from "eve/channels";
2
2
  import type { TelegramCallbackQuery, TelegramChannelEvents, TelegramContext, TelegramEventContext } from "eve/channels/telegram";
3
- import { type ObservedMessages, type PendingConfirmation, type RecentMessage } from "./observed.js";
3
+ import { type ConfirmationPrompt, type PendingConfirmation, type RecentMessage } from "../group-routing/messages.js";
4
4
  import { type Conversations } from "../conversations.js";
5
5
  import type { AgentIdentities } from "../identities.js";
6
6
  import { type PendingQuestion } from "../questions.js";
7
7
  import { type HistoryReader, type RecordedEvent } from "./sessions.js";
8
8
  export declare function confirmationCallback(requestId: string, memberId: string, decision: "approve" | "cancel" | "edit"): string;
9
9
  /** Terminal facts override stale presentation, including when a Telegram edit failed. */
10
- export declare function routingConfirmationState(events: readonly RecordedEvent[], prompt: import("./observed.js").ConfirmationPrompt, at: string): NonNullable<PendingConfirmation["state"]>;
10
+ export declare function routingConfirmationState(events: readonly RecordedEvent[], prompt: ConfirmationPrompt, at: string): NonNullable<PendingConfirmation["state"]>;
11
11
  /** Read-only projection of the same durable approval lifecycle used by the callback gate. */
12
12
  export declare function routingConfirmations(events: readonly RecordedEvent[], recent: readonly RecentMessage[], at: string): ReadonlyArray<PendingConfirmation>;
13
13
  export declare function questionFromHistory(events: readonly RecordedEvent[], owner?: Pick<PendingQuestion, "memberId" | "chatId" | "messageThreadId">): PendingQuestion | undefined;
@@ -23,5 +23,5 @@ export declare function handleConfirmationCallback(input: {
23
23
  readonly scope: "chat" | "thread";
24
24
  readonly now?: number;
25
25
  }): Promise<void>;
26
- export declare function telegramConfirmationEvents(archive?: Conversations, observed?: ObservedMessages, scope?: () => "chat" | "thread"): TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.settled">;
26
+ export declare function telegramConfirmationEvents(archive?: Conversations, scope?: () => "chat" | "thread"): TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.settled">;
27
27
  export declare const confirmationEvents: TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.settled">;
@@ -11,7 +11,7 @@ import { renderConfirmation, withConfirmationText } from "./confirmation-text.mj
11
11
  import { chatPostCard, chatPostCardStatus, chatPostReceipt, withChatPostCard } from "./chat-post-card.mjs";
12
12
  import { conversationEvent } from "../conversations.mjs";
13
13
  import { n as currentInboundRequest, p as reportTelegramError } from "../../../_chunks/inbound-request-D1r0FkDI.mjs";
14
- import { agentMessage, observedChatKey, withConfirmationPrompt } from "../group-routing/observed.mjs";
14
+ import { withConfirmationPrompt } from "../group-routing/messages.mjs";
15
15
  import { chatPostOperationId, isChatPost } from "../chat-post-confirmation.mjs";
16
16
  import { confirmations } from "../confirmations.mjs";
17
17
  import { formatAddress, scopedAddress } from "./sessions.mjs";
@@ -201,23 +201,7 @@ function messagePrompt(message) {
201
201
  at: new Date().toISOString()
202
202
  };
203
203
  }
204
- function telegramConfirmationEvents(archive, observed, scope) {
205
- const remember = (channel, ctx, messageId, text, confirmation) => {
206
- const principal = ctx.session.auth.current;
207
- const name = humanText(principal?.attributes.memberName);
208
- observed?.remember(observedChatKey(channel.telegram), {
209
- ...agentMessage({
210
- text,
211
- at: new Date().toISOString(),
212
- messageId,
213
- ...principal?.subject === void 0 || name === void 0 ? {} : { inReplyTo: {
214
- id: principal.subject,
215
- author: name
216
- } }
217
- }),
218
- ...confirmation === void 0 ? {} : { confirmation }
219
- });
220
- };
204
+ function telegramConfirmationEvents(archive, scope) {
221
205
  const record = async (callId, phase, text, status, ctx) => {
222
206
  const saved = confirmations.get()[callId];
223
207
  if (archive === void 0 || saved?.description?.chatPost === void 0 || saved.source === void 0) return;
@@ -275,7 +259,6 @@ function telegramConfirmationEvents(archive, observed, scope) {
275
259
  selective: true
276
260
  }
277
261
  }));
278
- remember(channel, ctx, sent.id, text);
279
262
  channel.state.pendingFreeformReplies = {
280
263
  ...channel.state.pendingFreeformReplies,
281
264
  [sent.id]: requestId
@@ -341,7 +324,6 @@ function telegramConfirmationEvents(archive, observed, scope) {
341
324
  } }
342
325
  };
343
326
  const sent = await withConfirmationPrompt(prompt, () => exactPost ? withChatPostCard(text, send, copy.previewLinks) : withConfirmationText(text, send));
344
- remember(channel, ctx, sent.id, text, prompt);
345
327
  if (aliasesPendingTopic(scope?.(), channel.continuation?.token)) channel.continuation?.alias(formatAddress({
346
328
  chatId: channel.telegram.chatId,
347
329
  messageThreadId: String(channel.telegram.messageThreadId ?? ""),
@@ -405,7 +387,6 @@ function telegramConfirmationEvents(archive, observed, scope) {
405
387
  };
406
388
  message.prompt = prompt;
407
389
  await withConfirmationPrompt(prompt, () => message.exactPost ? withChatPostCard(text, edit, [...message.previewLinks ?? [], ...telegramTextLinks(status)]) : withConfirmationText(text, edit));
408
- remember(channel, ctx, message.messageId, text, prompt);
409
390
  await record(callId, reason ?? (resolution.outcome === "accepted" ? "approved" : resolution.outcome === "declined" ? "cancelled" : "ignored"), text, resolution.outcome === "accepted" ? "requested" : "cancelled", ctx);
410
391
  if (resolution.outcome !== "accepted") {
411
392
  delete state.confirmationMessages?.[callId];
@@ -451,14 +432,13 @@ function telegramConfirmationEvents(archive, observed, scope) {
451
432
  }));
452
433
  channel.continuation?.alias(questionAddress(saved));
453
434
  }
454
- const sent = await channel.telegram.post({
435
+ await channel.telegram.post({
455
436
  text: question.question,
456
437
  reply_markup: {
457
438
  force_reply: true,
458
439
  selective: true
459
440
  }
460
441
  });
461
- remember(channel, ctx, sent.id, question.question);
462
442
  }
463
443
  }
464
444
  const message = state.confirmationMessages?.[data.callId];
@@ -479,7 +459,6 @@ function telegramConfirmationEvents(archive, observed, scope) {
479
459
  replyMarkup: { inline_keyboard: [] },
480
460
  text
481
461
  }), [...message.previewLinks ?? [], ...telegramTextLinks(status)]));
482
- remember(channel, ctx, message.messageId, text, prompt);
483
462
  await record(data.callId, "receipt", text, failed ? "failed" : object(action?.kind === "tool" ? data.output : void 0).status === "sent" ? "completed" : "unknown", ctx);
484
463
  } else {
485
464
  const copy = message.copy ?? renderConfirmation();
@@ -495,7 +474,6 @@ function telegramConfirmationEvents(archive, observed, scope) {
495
474
  replyMarkup: { inline_keyboard: [] },
496
475
  text
497
476
  })));
498
- remember(channel, ctx, message.messageId, text, prompt);
499
477
  }
500
478
  delete state.confirmationMessages?.[data.callId];
501
479
  },
@@ -2,7 +2,6 @@ import type { StructuredMessage } from "./structured.js";
2
2
  import type { AgentIdentities } from "../identities.js";
3
3
  import { type Conversations } from "../conversations.js";
4
4
  import type { AgentChats } from "./chats.js";
5
- import type { ObservedMessages } from "./observed.js";
6
5
  import type { DecodedAttachment } from "./attachment-description.js";
7
6
  import type { AddressingReason, RouteDecider, RouteDecision } from "./router.js";
8
7
  /** A crashed routing attempt can be reclaimed; the per-message turn claim still fences dispatch. */
@@ -14,7 +13,6 @@ export declare function captureTelegramWebhook(input: {
14
13
  readonly request: Request;
15
14
  readonly archive: Conversations;
16
15
  readonly identities: AgentIdentities;
17
- readonly observed: ObservedMessages;
18
16
  readonly chats?: AgentChats;
19
17
  readonly next: (request?: Request) => Promise<Response>;
20
18
  }): Promise<Response>;
@@ -22,11 +20,25 @@ export declare function captureTelegramWebhook(input: {
22
20
  export declare function recordGateDecision(reason: string, decision?: "reply" | "silent"): void;
23
21
  export declare function recordRouteDecision(route: "reply" | "observe", addressed: boolean | AddressingReason | undefined, policy: string, audit?: Pick<RouteDecision, "reason" | "note" | "diagnostics" | "router" | "shadow">): void;
24
22
  export declare function captureRouteDecider(decide: RouteDecider): RouteDecider;
23
+ /** A stored attachment of an archived message: what a later turn needs to read or show it. */
24
+ export interface ArchivedFile {
25
+ readonly id: string;
26
+ readonly filename: string;
27
+ readonly contentType: string;
28
+ readonly size: number;
29
+ readonly expiresAt: string;
30
+ /** Whether it can reach a model as a native image. */
31
+ readonly image?: true;
32
+ }
25
33
  /** Update the existing inbound event after STT/description, under the same text retention policy. */
26
34
  export declare function recordInboundContent(content: {
27
35
  readonly text: string;
28
36
  readonly media?: string;
29
37
  readonly decoded?: ReadonlyArray<DecodedAttachment>;
38
+ /** How the chat context names a linked member: their name and roles, then their handle. */
39
+ readonly author?: string;
40
+ /** The message's stored attachments, by reference: never bytes or download grants. */
41
+ readonly files?: ReadonlyArray<ArchivedFile>;
30
42
  }): Promise<void>;
31
43
  /**
32
44
  * Records eve's unrecoverable session failure from the channel. The channel receives it even when
@@ -54,7 +66,6 @@ export declare function telegramConversationCapture(input: {
54
66
  readonly structured?: (raw: Readonly<Record<string, unknown>>) => StructuredMessage | undefined;
55
67
  readonly archive: Conversations;
56
68
  readonly identities: () => AgentIdentities;
57
- readonly observed: ObservedMessages;
58
69
  readonly chats: AgentChats;
59
70
  readonly verify: (request: Request) => boolean;
60
71
  }): (request: Request, next: (request?: Request) => Promise<Response>) => Promise<Response>;
@@ -6,12 +6,12 @@ __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { n as contentText, t as CONTENT_CAPS } from "../../../_chunks/content-cDtVnr8Y.mjs";
8
8
  import { conversationEvent, sessionFailureEventId } from "../conversations.mjs";
9
+ import { currentDeliveryScope } from "./delivery-diagnostics.mjs";
9
10
  import { n as currentInboundRequest, p as reportTelegramError, s as currentThreadId } from "../../../_chunks/inbound-request-D1r0FkDI.mjs";
10
- import { currentConfirmationPrompt, observedChatKey } from "../group-routing/observed.mjs";
11
- import { archivedRoutingMessages } from "../group-routing/routing-context.mjs";
11
+ import { currentConfirmationPrompt } from "../group-routing/messages.mjs";
12
12
  import { parseArchivedMessage } from "./archive-message.mjs";
13
13
  import { contentShape, messageContent } from "./content.mjs";
14
- import { messageMetadata } from "./observed.mjs";
14
+ import { messageMetadata } from "./message-metadata.mjs";
15
15
  import { reportMembership } from "./membership.mjs";
16
16
  import { createHash, randomUUID } from "node:crypto";
17
17
  const captures = new WeakMap();
@@ -177,20 +177,8 @@ async function captureTelegramWebhook(input) {
177
177
  };
178
178
  const inbound = currentInboundRequest();
179
179
  if (inbound !== void 0) captures.set(inbound, capture);
180
- const chat = observedChatKey({
181
- chatId: message.chat.id,
182
- ...message.messageThreadId === void 0 ? {} : { messageThreadId: message.messageThreadId }
183
- });
184
180
  const automated = message.from === void 0 || message.from.isBot || message.chat.type === "channel";
185
181
  if (automated || edited !== void 0 || message.raw.left_chat_member || message.raw.migrate_to_chat_id || message.raw.migrate_from_chat_id) await capture.persist();
186
- const history = automated ? void 0 : await input.archive.context({
187
- provider: "telegram",
188
- chatId: message.chat.id,
189
- ...event.threadId === null ? {} : { threadId: event.threadId },
190
- limit: 100,
191
- includeDecisions: true,
192
- ...lookup?.status === "paired" ? { participantId: lookup.member.id } : {}
193
- });
194
182
  const messageHistory = automated ? [] : (await input.archive.context({
195
183
  provider: "telegram",
196
184
  chatId: message.chat.id,
@@ -265,35 +253,16 @@ async function captureTelegramWebhook(input) {
265
253
  await recordInboundContent({
266
254
  text: [content.text, ...transcripts].filter(Boolean).join("\n\n"),
267
255
  decoded,
268
- ...media === void 0 ? {} : { media }
256
+ ...media === void 0 ? {} : { media },
257
+ ...Array.isArray(previousRouting.files) ? { files: previousRouting.files } : {}
269
258
  });
270
259
  }
271
260
  }
272
- if (!automated) for (const earlier of input.observed.recent(chat).length === 0 ? archivedRoutingMessages((history?.events ?? []).filter((item) => item.eventId !== event.eventId), 60) : []) if ("role" in earlier || history?.events.some((decision) => decision.kind === "decision" && decision.messageId === earlier.messageId && decision.reason === "turn_admitted")) input.observed.remember(chat, earlier);
273
- else input.observed.add(chat, earlier);
274
261
  if (automated || edited !== void 0) {
275
262
  if (message.chat.type === "group" || message.chat.type === "supergroup") await input.chats?.seen({
276
263
  chatId: message.chat.id,
277
264
  title: message.chat.title ?? ""
278
265
  });
279
- const remembered = input.observed.recent(chat).findLast((entry) => entry.messageId === message.messageId);
280
- const retained = alreadyHandled && edited === void 0 ? remembered : void 0;
281
- const retainedContent = archivedRoutingMessages([{
282
- ...capture.event,
283
- sequence: "1",
284
- storedAt: event.at
285
- }])[0];
286
- input.observed.add(chat, retained ?? (retainedContent === void 0 ? void 0 : {
287
- ...retainedContent,
288
- ...unchangedAttachments && remembered?.images !== void 0 ? { images: remembered.images } : {}
289
- }) ?? {
290
- ...messageMetadata(message),
291
- author: event.senderName ?? event.senderId ?? "automated post",
292
- text: event.text ?? "[attachment or service message]",
293
- at: event.at,
294
- messageId: message.messageId,
295
- ...event.senderId === null ? {} : { authorId: event.senderId }
296
- });
297
266
  capture.decision = "silent";
298
267
  capture.reason = automated ? "automated_or_channel_message" : alreadyHandled ? "message_already_handled" : "message_edit";
299
268
  if (alreadyHandled) capture.note = "This message already started a turn; edits and retries do not repeat its task.";
@@ -495,7 +464,9 @@ async function recordInboundContent(content) {
495
464
  await capture.persist();
496
465
  const metadata = record(JSON.parse(capture.event.argsSummary ?? "{}"));
497
466
  const membership = capture.lookup === void 0 ? record(metadata.routing).membership : capture.lookup.status === "paired" || capture.lookup.status === "existing" || capture.lookup.status === "created" ? "linked" : "unlinked";
498
- if (content.text === capture.event.text && content.media === record(metadata.routing).media && membership === record(metadata.routing).membership && JSON.stringify(content.decoded) === JSON.stringify(record(metadata.routing).decoded)) return;
467
+ const author = content.author ?? record(metadata.routing).author;
468
+ const files = content.files ?? record(metadata.routing).files;
469
+ if (content.text === capture.event.text && content.media === record(metadata.routing).media && membership === record(metadata.routing).membership && author === record(metadata.routing).author && JSON.stringify(files) === JSON.stringify(record(metadata.routing).files) && JSON.stringify(content.decoded) === JSON.stringify(record(metadata.routing).decoded)) return;
499
470
  const contentHash = createHash("sha256").update(JSON.stringify({
500
471
  ...content,
501
472
  membership
@@ -509,6 +480,8 @@ async function recordInboundContent(content) {
509
480
  routing: {
510
481
  ...record(metadata.routing),
511
482
  membership,
483
+ ...typeof author === "string" ? { author: author.slice(0, 500) } : {},
484
+ ...Array.isArray(files) && files.length > 0 ? { files: files.slice(0, 10) } : {},
512
485
  media: content.media?.slice(0, 8e3),
513
486
  decoded: content.decoded
514
487
  }
@@ -582,6 +555,8 @@ function captureTelegramFetch(fetchImpl, archive, identities) {
582
555
  const chatId = id(body.chat_id);
583
556
  if (chatId === null) return fetchImpl(input, init);
584
557
  const { capture, chatType, participantIds } = await outboundAudience(chatId, identities);
558
+ const delivery = currentDeliveryScope();
559
+ const own = delivery?.chatId === chatId && delivery.sessionId !== null;
585
560
  const event = conversationEvent({
586
561
  eventId: `telegram:${edit ? "edit" : "send"}:${randomUUID()}`,
587
562
  provider: "telegram",
@@ -620,6 +595,10 @@ function captureTelegramFetch(fetchImpl, archive, identities) {
620
595
  participantIds,
621
596
  threadId: id(body.message_thread_id) ?? currentThreadId() ?? null,
622
597
  replyToMessageId: id(body.reply_to_message_id) ?? id(record(body.reply_parameters).message_id),
598
+ ...own ? {
599
+ sessionId: delivery.sessionId,
600
+ turnId: delivery.turnId
601
+ } : {},
623
602
  status: "requested"
624
603
  });
625
604
  await archive.append(event);
@@ -5,8 +5,11 @@ interface Diagnostic {
5
5
  readonly error: unknown;
6
6
  readonly metadata: Record<string, unknown>;
7
7
  }
8
+ type DeliveryScope = Pick<ConversationEvent, "chatId" | "chatType" | "threadId" | "sessionId" | "turnId">;
8
9
  /** Receives the same credential-redacted payload as operator logs, never raw provider errors. */
9
10
  export declare function retainDeliveryDiagnostic(diagnostic: Diagnostic): void;
11
+ /** The session and turn a channel callback delivers for, so its sends are archived as its own. */
12
+ export declare function currentDeliveryScope(): DeliveryScope | undefined;
10
13
  /** Await archive writes inside the channel callback, including failed or restored callbacks. */
11
- export declare function withDeliveryDiagnostics<T>(archive: Conversations, scope: Pick<ConversationEvent, "chatId" | "chatType" | "threadId" | "sessionId" | "turnId">, run: () => T | Promise<T>): Promise<T>;
14
+ export declare function withDeliveryDiagnostics<T>(archive: Conversations, scope: DeliveryScope, run: () => T | Promise<T>): Promise<T>;
12
15
  export {};
@@ -10,11 +10,17 @@ import { conversationEvent } from "../conversations.mjs";
10
10
  import { AsyncLocalStorage } from "node:async_hooks";
11
11
  const pending = new AsyncLocalStorage();
12
12
  function retainDeliveryDiagnostic(diagnostic) {
13
- pending.getStore()?.push(diagnostic);
13
+ pending.getStore()?.diagnostics.push(diagnostic);
14
+ }
15
+ function currentDeliveryScope() {
16
+ return pending.getStore()?.scope;
14
17
  }
15
18
  async function withDeliveryDiagnostics(archive, scope, run) {
16
19
  const diagnostics = [];
17
- return pending.run(diagnostics, async () => {
20
+ return pending.run({
21
+ diagnostics,
22
+ scope
23
+ }, async () => {
18
24
  try {
19
25
  return await run();
20
26
  } finally {
@@ -43,4 +49,4 @@ async function withDeliveryDiagnostics(archive, scope, run) {
43
49
  }
44
50
  });
45
51
  }
46
- export { retainDeliveryDiagnostic, withDeliveryDiagnostics };
52
+ export { currentDeliveryScope, retainDeliveryDiagnostic, withDeliveryDiagnostics };
@@ -5,7 +5,7 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { runtimeRequestFailure } from "./runtime-request.mjs";
8
- import { t as agentBuild } from "../../../_chunks/build-metadata-D_Gqlb5f.mjs";
8
+ import { t as agentBuild } from "../../../_chunks/build-metadata-CMmCRD8l.mjs";
9
9
  import { GET } from "eve/channels";
10
10
  const HEALTH_ROUTE = "/eve/v1/telegram/health";
11
11
  function healthRoute(input) {