@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
@@ -1,5 +1,5 @@
1
1
  import { type StructuredInbound, type StructuredMessage, type StructuredResult } from "./structured.js";
2
- import type { RecentMessage } from "../group-routing/observed.js";
2
+ import type { RecentMessage } from "../group-routing/messages.js";
3
3
  import { type GroupPolicy } from "../group-routing/policy.js";
4
4
  import type { TelegramSettings } from "../../config.js";
5
5
  import { attachmentLifecycle } from "./lifecycle.js";
@@ -12,9 +12,8 @@ import { type AgentChats } from "./chats.js";
12
12
  import type { InboundRequest } from "./inbound-request.js";
13
13
  import type { ImageDescriber } from "./images.js";
14
14
  import type { MediaPlan } from "./media-policy.js";
15
- import { type ObservedMessages } from "./observed.js";
16
15
  import { type RouteDecider } from "./router.js";
17
- import { type HistoryReader, type SessionPolicy } from "./sessions.js";
16
+ import { type HistoryReader } from "./sessions.js";
18
17
  import { type Transcriber } from "./voice.js";
19
18
  import { type AttachmentDescriber } from "./attachment-description.js";
20
19
  import type { PdfTextExtractor } from "./pdf-text.js";
@@ -30,16 +29,18 @@ import type { PdfTextExtractor } from "./pdf-text.js";
30
29
  * 2. Voice becomes text, so the router and the agent read what was said.
31
30
  * 3. The router answers or observes. Group files follow the same addressing rules as text;
32
31
  * captionless uploads use smart routing and recent agent requests. Files are stored after the
33
- * registry knows the sender. Observing starts no session or typing indication;
34
- * the ring carries the stored files' references (or storage failures) into the next reply.
35
- * 4. The session policy runs on the session an answered message continues: an idle one is cleared, the
36
- * first message after the day's compaction time compacts it.
32
+ * registry knows the sender. Observing starts no session or typing indication; the
33
+ * conversation archive keeps the message and its stored files' references for routing, the
34
+ * next turn's chat context and the native images that turn carries.
35
+ * 4. An answered message continues the session its address names under the configured scope; eve
36
+ * compacts and expires sessions itself.
37
37
  * 5. An answered message reads the stored attachments in place of the Telegram files.
38
- * 6. An answered message dispatches as the paired member, with the ring, the registry's `<chat>`
39
- * block, a `<speaker>` block naming the member, their roles and the message they replied to and
40
- * an `<attachments>` block naming its stored files ahead of it as context. An answered group
41
- * message waits for its chat's report first, so that a chat new to the process already carries
42
- * its `<chat>` block.
38
+ * 6. An answered message dispatches as the paired member, with only its own facts as context: a
39
+ * `<speaker>` block naming the member, their roles and the message they replied to, its
40
+ * `<message_audience>` and an `<attachments>` block naming its stored files. The chat around
41
+ * it, observed messages and the registry's `<chat>` guidance, reaches the session once through
42
+ * its chat context (`lib/chat-context.ts`). An answered group message waits for its chat's
43
+ * report first, so that a chat new to the process already carries its guidance.
43
44
  */
44
45
  export interface InboundDependencies {
45
46
  readonly structured?: {
@@ -49,12 +50,12 @@ export interface InboundDependencies {
49
50
  readonly policy: TelegramPolicy;
50
51
  readonly decisionModel?: string;
51
52
  readonly groups?: GroupPolicy;
53
+ /** The chat's or forum topic's recent messages from the conversation archive, oldest first. */
52
54
  readonly recentConversation?: (chatId: string, threadId?: string) => Promise<ReadonlyArray<RecentMessage>>;
53
55
  readonly signalSettings?: TelegramSettings["groups"];
54
56
  readonly lifecycle?: (principal: import("eve/context").SessionAuthContext, memberId: string, message: TelegramMessage) => ReturnType<typeof attachmentLifecycle>;
55
57
  readonly admission: () => AdmissionSettings;
56
58
  readonly sessions: () => SessionSettings;
57
- readonly sessionPolicy: SessionPolicy;
58
59
  readonly sttModel: string | false;
59
60
  readonly attachmentsModel: string | false;
60
61
  readonly describeAttachment: AttachmentDescriber;
@@ -73,7 +74,6 @@ export interface InboundDependencies {
73
74
  }) => Promise<Uint8Array>;
74
75
  /** The attachment store; without it, files reach the turn as eve's Telegram file parts. */
75
76
  readonly attachments?: AgentAttachments;
76
- readonly observed: ObservedMessages;
77
77
  /** Recent session events for smart routing. */
78
78
  readonly readHistory: HistoryReader;
79
79
  readonly chats: AgentChats;
@@ -9,29 +9,28 @@ import { admitMessage, localizedText } from "../admission.mjs";
9
9
  import { d as telegramRuntimeSource, n as conversationLanguage, o as telegramPrincipal, u as runtimeEnvelope } from "../../../_chunks/principal-D99Lleae.mjs";
10
10
  import { p as reportTelegramError } from "../../../_chunks/inbound-request-D1r0FkDI.mjs";
11
11
  import { addressingReason, conversationKey, otherRecipient } from "../group-routing/signals.mjs";
12
- import { conversationAddressing, conversationState } from "../group-routing/conversation.mjs";
12
+ import { conversationAddressing } from "../group-routing/addressing.mjs";
13
13
  import { questionInvitation } from "../group-routing/engagement.mjs";
14
- import { conversationContext, routeMessage } from "../group-routing/router.mjs";
15
- import { observedContext } from "../group-routing/observed.mjs";
14
+ import { routeMessage } from "../group-routing/router.mjs";
16
15
  import { debounceBurst, routingBurst } from "../group-routing/burst.mjs";
17
16
  import { WorkspaceActionAlreadyDispatched } from "./workspace-action.mjs";
18
17
  import { structuredMessage } from "./structured.mjs";
19
- import { evidenceSince, formatAddress, messageAddress, scopedAddress } from "./sessions.mjs";
18
+ import { formatAddress, messageAddress, scopedAddress } from "./sessions.mjs";
20
19
  import { AttachmentLifecycleNotPermitted } from "./lifecycle.mjs";
21
20
  import { MAXIMUM_NATIVE_IMAGE_BYTES } from "./images.mjs";
22
21
  import { MAXIMUM_ROUTING_ATTACHMENT_BYTES, describeRoutingAttachment, routingAttachmentDeadline } from "./attachment-description.mjs";
23
22
  import { attachmentUrl } from "../attachments.mjs";
24
23
  import { contentShape, messageContent } from "./content.mjs";
25
24
  import { messageAudience } from "./audience.mjs";
26
- import { attachmentKind, messageMetadata } from "./observed.mjs";
27
- import { telegramSignals } from "./signals.mjs";
25
+ import { attachmentKind, messageMetadata } from "./message-metadata.mjs";
26
+ import { recordGateDecision, recordInboundContent, recordRouteDecision } from "./conversations.mjs";
28
27
  import { answerMarker } from "../questions.mjs";
29
28
  import { questionAddress, questionFromHistory } from "./confirmations.mjs";
30
- import { routingContext } from "./routing-context.mjs";
31
- import { recordGateDecision, recordInboundContent, recordRouteDecision } from "./conversations.mjs";
32
29
  import { chatContext } from "./chats.mjs";
30
+ import { telegramSignals } from "./signals.mjs";
33
31
  import { attachmentsContext, inboundAttachments, stageFiles } from "./files.mjs";
34
32
  import { heldApprovals, releaseHeldApproval } from "./held-approvals.mjs";
33
+ import { routingContext } from "./routing-context.mjs";
35
34
  import { voiceOf, voiceText } from "./voice.mjs";
36
35
  const MAXIMUM_QUOTE_LENGTH = 500;
37
36
  function buttonTo(text, url) {
@@ -68,6 +67,10 @@ function observedMessage(message, text, member, media, bot) {
68
67
  ...media ? { media } : {}
69
68
  };
70
69
  }
70
+ function observedImages(recent, current) {
71
+ const answered = recent.findLastIndex((message) => message.admitted === true);
72
+ return recent.slice(answered + 1).filter((message) => !("role" in message) && message.messageId !== current).flatMap((message) => message.images ?? []);
73
+ }
71
74
  function isRecord(value) {
72
75
  return typeof value === "object" && value !== null && !Array.isArray(value);
73
76
  }
@@ -268,7 +271,19 @@ async function handleInboundMessage(input) {
268
271
  const turn = structured === void 0 && input.request?.structuredContext === void 0 && addressed !== void 0 && hasContent && (addressed !== "reply-to-bot" || elsewhere === void 0);
269
272
  const lookup = await resolve(turn);
270
273
  const group = isGroupChat(sent);
271
- const chat = conversationKey(signals);
274
+ const freshConversation = async () => await dependencies.recentConversation?.(sent.chat.id, sent.messageThreadId === void 0 ? void 0 : String(sent.messageThreadId)) ?? [];
275
+ let archived;
276
+ const recentConversation = () => archived ??= freshConversation();
277
+ const languageOf = async (current) => {
278
+ const own = conversationLanguage([current]);
279
+ if (own !== void 0) return own;
280
+ try {
281
+ return conversationLanguage((await recentConversation()).map((message) => message.text).reverse());
282
+ } catch (error) {
283
+ reportTelegramError("conversation_language_unavailable", error, { chatId: sent.chat.id });
284
+ return;
285
+ }
286
+ };
272
287
  const report = group ? dependencies.chats.seen({
273
288
  chatId: sent.chat.id,
274
289
  title: sent.chat.title ?? "",
@@ -349,9 +364,9 @@ async function handleInboundMessage(input) {
349
364
  async function observeRaw() {
350
365
  await recordInboundContent({
351
366
  text,
352
- decoded
367
+ decoded,
368
+ author: authorOf(sent.from, void 0)
353
369
  });
354
- if (group) dependencies.observed.add(chat, observedMessage(sent, text || "[attachment]", void 0, void 0, bot));
355
370
  }
356
371
  if (lookup.status !== "existing" && lookup.status !== "created") {
357
372
  const configuredGroupPairing = lookup.status === "pairing_required" && localizedText(dependencies.admission().replies.groupPairingRequired, sent.from.languageCode) !== void 0;
@@ -360,6 +375,10 @@ async function handleInboundMessage(input) {
360
375
  return null;
361
376
  }
362
377
  let member = lookup.member;
378
+ const recordContent = (content) => recordInboundContent({
379
+ ...content,
380
+ author: authorOf(sent.from, member)
381
+ });
363
382
  if (input.request?.structuredContext !== void 0 && input.request.structuredContext.memberId !== member.id) {
364
383
  recordGateDecision("structured_context_actor_changed");
365
384
  return null;
@@ -393,7 +412,7 @@ async function handleInboundMessage(input) {
393
412
  const principal = telegramPrincipal({
394
413
  message: sent,
395
414
  member,
396
- conversationLanguage: conversationLanguage([sent.text || sent.caption, ...dependencies.observed.recent(chat).map((message) => message.text).reverse()])
415
+ conversationLanguage: await languageOf(sent.text || sent.caption)
397
416
  });
398
417
  input.request.lifecycle = dependencies.lifecycle(principal, member.id, sent);
399
418
  await context.telegram.startTyping();
@@ -481,8 +500,7 @@ async function handleInboundMessage(input) {
481
500
  bot?.firstName,
482
501
  bot?.displayName
483
502
  ].filter((n) => !!n)];
484
- const freshConversation = async () => dependencies.recentConversation ? dependencies.recentConversation(sent.chat.id, sent.messageThreadId === void 0 ? void 0 : String(sent.messageThreadId)) : dependencies.observed.recent(chat);
485
- let conversationRecent = conversationPolicy ? await freshConversation() : dependencies.observed.recent(chat);
503
+ let conversationRecent = conversationPolicy ? await recentConversation() : [];
486
504
  if (conversationPolicy && input.request && input.request.lifecycleKey === void 0 && input.request.structuredContext === void 0) {
487
505
  address = topicAddress;
488
506
  input.request.address = topicAddress;
@@ -497,26 +515,16 @@ async function handleInboundMessage(input) {
497
515
  });
498
516
  const questionAge = Date.parse(sentAt(sent)) - Date.parse(asked?.meta.at ?? "");
499
517
  if (answering && addressed === void 0 && !questionInvitation(asked?.meta.at, sentAt(sent), groups.smart.engagement.openQuestionMinutes)) answering = false;
500
- if (conversationPolicy && answering) {
501
- if (!conversationState({
502
- signals: policySignals,
503
- recent: conversationRecent,
504
- policy: groups,
505
- agentNames: agentAliases,
506
- pendingQuestionAt: asked?.meta.at
507
- }).pending_question_live) answering = false;
508
- }
509
518
  const recipientAnswer = !conversationPolicy && answering && (groups.smart.engagement.policy === "engaged" || questionAge >= 0 && questionAge <= groups.smart.engagement.followUpMinutes * 6e4) && audience.replyTo?.kind === "bot" && isRecipientAnswer(audience);
510
519
  if (!conversationPolicy && answering && pending !== void 0 && input.request !== void 0) {
511
520
  address = pending.address || (questionSession === void 0 ? topicAddress : memberQuestionAddress);
512
521
  input.request.address = address;
513
522
  }
514
523
  if (input.request?.lifecycleKey === void 0 && input.request?.structuredContext === void 0 && group && (addressed === void 0 && !answering && groups.mode === "mention" || !conversationPolicy && elsewhere !== void 0 && (addressed === void 0 || addressed === "reply-to-bot" && !recipientAnswer))) {
515
- await recordInboundContent({
516
- text,
524
+ await recordContent({
525
+ text: text || (structured === void 0 ? text : `[${structured.kind}]`),
517
526
  decoded
518
527
  });
519
- dependencies.observed.add(chat, observedMessage(sent, text || (structured === void 0 ? "[attachment]" : `[${structured.kind}]`), member, void 0, bot));
520
528
  recordRouteDecision("observe", addressed, groups.mode, {
521
529
  reason: elsewhere === void 0 ? "mention_policy" : "other_recipient",
522
530
  note: elsewhere ?? "Mention policy observes unaddressed messages."
@@ -531,7 +539,12 @@ async function handleInboundMessage(input) {
531
539
  await observeRaw();
532
540
  return null;
533
541
  }
534
- const mediaPlan = voice !== void 0 || sent.attachments.length > 0 || dependencies.observed.list(chat).some((message) => (message.images?.length ?? 0) > 0) ? await dependencies.mediaPlan?.(member, owner?.id) : void 0;
542
+ if (group && !conversationPolicy) conversationRecent = (await recentConversation()).filter((message) => message.messageId !== sent.messageId).map(({ signedAuthor, ...message }) => signedAuthor === void 0 ? message : {
543
+ ...message,
544
+ author: signedAuthor
545
+ });
546
+ const carried = observedImages(conversationRecent, sent.messageId);
547
+ const mediaPlan = voice !== void 0 || sent.attachments.length > 0 || carried.length > 0 ? await dependencies.mediaPlan?.(member, owner?.id) : void 0;
535
548
  if (voice !== void 0 && !group) await transcribeVoice(false);
536
549
  if (voice !== void 0 && mediaPlan?.audio) notes.push("[The chat model supports audio, but this host cannot deliver native audio. The configured speech-to-text fallback was used if enabled.]");
537
550
  if (structured !== void 0) {
@@ -575,7 +588,7 @@ async function handleInboundMessage(input) {
575
588
  address,
576
589
  operations,
577
590
  read: dependencies.history ?? dependencies.readHistory,
578
- remembered: conversationPolicy ? conversationRecent : dependencies.observed.recent(chat),
591
+ remembered: conversationRecent,
579
592
  at: sentAt(sent),
580
593
  questionHistory,
581
594
  questionSessionId: owner?.id
@@ -625,7 +638,7 @@ async function handleInboundMessage(input) {
625
638
  };
626
639
  const evidence = conversationAddressing(routingSignals, conversationRecent, groups, agentAliases);
627
640
  if (!evidence.agent_named && !evidence.reply_to_agent && evidence.human_targets.length === 0) {
628
- await recordInboundContent({
641
+ await recordContent({
629
642
  text,
630
643
  decoded
631
644
  });
@@ -718,7 +731,7 @@ async function handleInboundMessage(input) {
718
731
  ...notes
719
732
  ].join("\n");
720
733
  } else if (voice !== void 0) media = [...attachmentMetadata, ...notes].join("\n");
721
- await recordInboundContent({
734
+ await recordContent({
722
735
  text,
723
736
  decoded,
724
737
  ...processedMedia === void 0 ? {} : { media: processedMedia }
@@ -743,11 +756,10 @@ async function handleInboundMessage(input) {
743
756
  let structuredDispatched = false;
744
757
  if (structured !== void 0 && dependencies.structured !== void 0) {
745
758
  if (route === "observe" || admissionRefused) {
746
- await recordInboundContent({
759
+ await recordContent({
747
760
  text,
748
761
  decoded
749
762
  });
750
- if (group) dependencies.observed.add(chat, observedMessage(sent, text, member, media, bot));
751
763
  return null;
752
764
  }
753
765
  if (!await admitTurn(true)) return null;
@@ -813,14 +825,12 @@ async function handleInboundMessage(input) {
813
825
  note: "The configured workspace action admitted this attachment task."
814
826
  });
815
827
  if (admissionRefused) {
816
- await recordInboundContent({
828
+ await recordContent({
817
829
  text,
818
830
  decoded
819
831
  });
820
- dependencies.observed.add(chat, observedMessage(sent, text || "[attachment]", member, void 0, bot));
821
832
  return null;
822
833
  }
823
- if (group && route === "observe") dependencies.observed.add(chat, observedMessage(sent, text, member, media, bot));
824
834
  if (sent.attachments.length > 0) await report;
825
835
  let files = input.request?.edited && !input.request.attachmentsChanged && (input.request.previousContent?.decoded.length ?? 0) > 0 || sent.attachments.length === 0 || dependencies.attachments === void 0 || input.request === void 0 ? new Map() : await stageFiles({
826
836
  message: sent,
@@ -937,24 +947,24 @@ async function handleInboundMessage(input) {
937
947
  if (input.request !== void 0 && (voice !== void 0 || processedMedia !== void 0)) input.request.text = text;
938
948
  const retainedMedia = processedMedia ?? ([...files.values()].flatMap((file) => file.status === "stored" && file.description !== void 0 ? [file.description] : []).join("\n") || void 0);
939
949
  if (decoded.length === 0) decoded.push(...[...files.values()].flatMap((file) => file.status === "stored" && file.decoded !== void 0 ? [file.decoded] : []));
940
- await recordInboundContent({
950
+ await recordContent({
941
951
  text,
942
952
  decoded,
943
- ...retainedMedia === void 0 ? {} : { media: retainedMedia }
953
+ ...retainedMedia === void 0 ? {} : { media: retainedMedia },
954
+ files: [...files.values()].flatMap((file) => file.status === "stored" ? [{
955
+ id: file.attachment.id,
956
+ filename: file.attachment.filename,
957
+ contentType: file.attachment.contentType,
958
+ size: file.attachment.size,
959
+ expiresAt: file.attachment.expiresAt,
960
+ ...file.imageInput === "native" ? { image: true } : {}
961
+ }] : [])
944
962
  });
945
- if (route === "observe") {
946
- const images = [...files.values()].flatMap((file) => file.status === "stored" && file.imageInput === "native" ? [file.attachment] : []);
947
- dependencies.observed.add(chat, {
948
- ...observedMessage(sent, text === "" ? "[attachment]" : text, member, media, bot),
949
- images
950
- });
951
- return null;
952
- }
963
+ if (route === "observe") return null;
953
964
  if (!structuredDispatched && input.request?.claimTurn !== void 0 && !await input.request.claimTurn()) {
954
965
  recordGateDecision("message_already_handled");
955
966
  return null;
956
967
  }
957
- let receivingHistory;
958
968
  if (operations !== void 0 && dependencies.history !== void 0) {
959
969
  let recoverySessionId;
960
970
  try {
@@ -962,7 +972,6 @@ async function handleInboundMessage(input) {
962
972
  if (session !== void 0) {
963
973
  recoverySessionId = session.id;
964
974
  const previous = session.id === owner?.id ? questionHistory : await dependencies.history(session.id);
965
- receivingHistory = previous;
966
975
  const recovery = heldApprovals(previous).length === 0 ? void 0 : await releaseHeldApproval({
967
976
  events: await dependencies.history(session.id),
968
977
  now: Date.now(),
@@ -996,24 +1005,6 @@ async function handleInboundMessage(input) {
996
1005
  notes.push(`<pending_question>\nTask: ${pending.task}\nQuestion: ${pending.question}\nUse this member's answer to continue the pending task with their current permissions.
997
1006
  </pending_question>`);
998
1007
  }
999
- if (operations !== void 0) {
1000
- const newest = dependencies.observed.list(chat).at(-1);
1001
- if (await dependencies.sessionPolicy.apply({
1002
- address,
1003
- operations,
1004
- ...newest === void 0 ? {} : { activeAt: Date.parse(newest.at) || 0 }
1005
- }) === "cleared") {
1006
- dependencies.observed.clear(chat);
1007
- question = void 0;
1008
- if (answering && input.request !== void 0) {
1009
- answering = false;
1010
- input.request.text = text;
1011
- const questionNote = notes.findIndex((note) => note.startsWith("<pending_question>"));
1012
- if (questionNote !== -1) notes.splice(questionNote, 1);
1013
- }
1014
- }
1015
- dependencies.sessionPolicy.delivered(address);
1016
- }
1017
1008
  await context.telegram.startTyping();
1018
1009
  await report;
1019
1010
  if (input.request !== void 0 && files.size > 0) input.request.files = files;
@@ -1021,7 +1012,7 @@ async function handleInboundMessage(input) {
1021
1012
  const currentImages = [...files.values()].flatMap((file) => file.status === "stored" && file.imageInput === "native" ? [file.attachment] : []);
1022
1013
  let remaining = Math.max(0, MAXIMUM_NATIVE_IMAGE_BYTES - currentImages.reduce((sum, file) => sum + file.size, 0));
1023
1014
  let count = currentImages.length;
1024
- input.request.extraFiles = dependencies.observed.list(chat).flatMap((message) => (message.images ?? []).flatMap((image) => {
1015
+ input.request.extraFiles = carried.flatMap((image) => {
1025
1016
  if (Date.parse(image.expiresAt) <= Date.now() || image.size > remaining || count >= 5) {
1026
1017
  notes.push(`[Earlier image ${JSON.stringify(image.filename)} omitted: expired or native context limit.]`);
1027
1018
  return [];
@@ -1038,16 +1029,15 @@ async function handleInboundMessage(input) {
1038
1029
  mediaType: image.contentType,
1039
1030
  filename: image.filename
1040
1031
  }];
1041
- }));
1032
+ });
1042
1033
  }
1043
- const language = conversationLanguage([text, ...dependencies.observed.recent(chat).map((message) => message.text).reverse()]);
1044
- const sharedContext = group ? conversationContext(await conversation(), { since: receivingHistory === void 0 ? void 0 : evidenceSince(receivingHistory) }) : void 0;
1034
+ const language = await languageOf(text);
1035
+ const confirmationReply = (group ? await conversation() : void 0)?.recent.some((entry) => "role" in entry && entry.confirmation !== void 0 && entry.messageId === audience.replyTo?.messageId) === true;
1045
1036
  const beginTurnDispatch = structuredDispatched ? input.request?.beginStructuredContinuation : input.request?.beginDispatch;
1046
1037
  if (beginTurnDispatch !== void 0 && !await beginTurnDispatch()) {
1047
1038
  recordGateDecision("message_already_handled");
1048
1039
  return null;
1049
1040
  }
1050
- dependencies.observed.remember(chat, observedMessage(sent, text, member, retainedMedia, bot));
1051
1041
  let principal = telegramPrincipal({
1052
1042
  message: {
1053
1043
  ...sent,
@@ -1056,6 +1046,14 @@ async function handleInboundMessage(input) {
1056
1046
  member,
1057
1047
  conversationLanguage: language
1058
1048
  });
1049
+ const [guidance] = group ? chatContext(dependencies.chats.chat(sent.chat.id)) : [];
1050
+ if (guidance !== void 0) principal = {
1051
+ ...principal,
1052
+ attributes: {
1053
+ ...principal.attributes,
1054
+ chatGuidance: guidance
1055
+ }
1056
+ };
1059
1057
  if (conversationPolicy && input.request?.lifecycleKey === void 0 && input.request?.structuredContext === void 0) principal = {
1060
1058
  ...principal,
1061
1059
  attributes: {
@@ -1109,27 +1107,24 @@ async function handleInboundMessage(input) {
1109
1107
  },
1110
1108
  context: [
1111
1109
  ...input.request?.lifecycleContext === void 0 ? [] : [input.request.lifecycleContext],
1112
- ...observedContext(dependencies.observed.take(chat).slice(-groups.smart.context.messages), sharedContext),
1113
- ...group ? chatContext(dependencies.chats.chat(sent.chat.id)) : [],
1114
1110
  speakerBlock({
1115
1111
  message: sent,
1116
1112
  member,
1117
1113
  bot,
1118
- omitQuote: group && question?.recent.some((entry) => "role" in entry && entry.confirmation !== void 0 && entry.messageId === audience.replyTo?.messageId)
1114
+ omitQuote: confirmationReply
1119
1115
  }),
1120
1116
  "<message_audience>\n" + JSON.stringify({
1121
1117
  bot_addressed: addressed !== void 0 || answering,
1122
1118
  bot_tagged: audience.botTagged,
1123
- reply_target: sharedContext === void 0 ? audience.replyTo : JSON.parse(sharedContext.reply_target),
1124
- tagged_users: audience.mentions
1125
- }) + "\nUse these user-ID and rich-mention signals for addressing.\n</message_audience>",
1126
- "Reply in the member's conversation language. After an action, use at most one short human sentence saying what changed and where, with a link to the record in the app. Confirmations already show their status. Never quote tool output, tool names, record ids, JSON, stack traces or internal errors; explain failures briefly with an error reference when available.",
1119
+ reply_target: confirmationReply && audience.replyTo !== null ? Object.fromEntries(Object.entries(audience.replyTo).filter(([key]) => key !== "text")) : audience.replyTo,
1120
+ tagged_users: audience.mentions,
1121
+ ...conversationPolicy ? { engagement_policy: "conversation" } : {}
1122
+ }) + "\n</message_audience>",
1127
1123
  ...attachmentsContext({
1128
1124
  files,
1129
1125
  group
1130
1126
  }),
1131
1127
  ...processedMedia === void 0 ? [] : [processedMedia],
1132
- ...conversationPolicy ? ["If this member declines your involvement or closes the conversation, output exactly [SILENT]. Do not acknowledge the rejection or perform actions."] : [],
1133
1128
  ...notes
1134
1129
  ]
1135
1130
  };
@@ -10,7 +10,7 @@ import { n as currentInboundRequest } from "../../../_chunks/inbound-request-D1r
10
10
  import { member } from "../actor.mjs";
11
11
  import { needsConfirmation } from "../confirmations.mjs";
12
12
  import { OntologyActionNotPermitted, actionToolName, ontologyTools } from "../ontology-tools.mjs";
13
- import { t as agentBuild } from "../../../_chunks/build-metadata-D_Gqlb5f.mjs";
13
+ import { t as agentBuild } from "../../../_chunks/build-metadata-CMmCRD8l.mjs";
14
14
  var AttachmentLifecycleNotPermitted = class extends Error {
15
15
  code = "attachment_lifecycle_not_permitted";
16
16
  };
@@ -1,14 +1,10 @@
1
- import { type ObservedMessages } from "../group-routing/observed.js";
2
1
  import type { TelegramMessage } from "eve/channels/telegram";
3
2
  import type { BotIdentity } from "./router.js";
4
- import type { ObservedMessage } from "../group-routing/observed.js";
5
- export * from "../group-routing/observed.js";
3
+ import type { ObservedMessage } from "../group-routing/messages.js";
6
4
  /** Eve represents non-image media as documents; retain the actual Telegram kind in context. */
7
5
  export declare function attachmentKind(message: TelegramMessage, file: {
8
6
  readonly fileId: string;
9
7
  readonly kind: string;
10
8
  }): string;
11
- /** Bounded provider structure, shared by the warm ring and archive reconstruction. No file grants. */
9
+ /** Bounded provider structure, archived with each message for routing. No file grants. */
12
10
  export declare function messageMetadata(message: TelegramMessage, bot?: BotIdentity): Pick<ObservedMessage, "invitation" | "kind" | "username" | "replyTo" | "taggedUsers" | "attachmentKinds" | "hasCaption" | "revision">;
13
- /** Compatibility boundary for callers that used Telegram's former unqualified ring keys. */
14
- export declare function observedMessages(capacity?: number): ObservedMessages;
@@ -4,7 +4,6 @@ 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 { OBSERVED_MESSAGES, agentMessage, currentConfirmationPrompt, observedChatKey, observedContext, observedMessages as observedMessages$1, withConfirmationPrompt } from "../group-routing/observed.mjs";
8
7
  import { messageAudience } from "./audience.mjs";
9
8
  const PROVIDER_MEDIA_KINDS = [
10
9
  "voice",
@@ -49,16 +48,4 @@ function messageMetadata(message, bot) {
49
48
  }
50
49
  };
51
50
  }
52
- function observedMessages(capacity) {
53
- const ring = observedMessages$1(capacity);
54
- const key = (value) => /^[a-z][a-z0-9_-]*:/u.test(value) ? value : `telegram:${value.replace(/:/u, "/")}`;
55
- return {
56
- add: (chat, message) => ring.add(key(chat), message),
57
- remember: (chat, message) => ring.remember(key(chat), message),
58
- list: (chat) => ring.list(key(chat)),
59
- recent: (chat) => ring.recent(key(chat)),
60
- take: (chat) => ring.take(key(chat)),
61
- clear: (chat) => ring.clear(key(chat))
62
- };
63
- }
64
- export { OBSERVED_MESSAGES, agentMessage, attachmentKind, currentConfirmationPrompt, messageMetadata, observedChatKey, observedContext, observedMessages, withConfirmationPrompt };
51
+ export { attachmentKind, messageMetadata };
@@ -1,10 +1,11 @@
1
- import { type RecentMessage, type PendingConfirmation } from "./observed.js";
1
+ import { type RecentMessage, type PendingConfirmation } from "../group-routing/messages.js";
2
2
  import type { HistoryReader, RecordedEvent, SessionOperations } from "./sessions.js";
3
3
  import { type PendingQuestion } from "../questions.js";
4
4
  export { archivedRoutingMessages } from "../group-routing/routing-context.js";
5
5
  /**
6
- * Recover the agent's questions from the existing session without starting a turn. The local
7
- * ring supplements this with recipient identities and chatter not written to Eve's stream.
6
+ * Recover the agent's questions from the existing session without starting a turn. The archived
7
+ * messages (`remembered`) supplement this with recipient identities and chatter not written to
8
+ * Eve's stream.
8
9
  */
9
10
  export declare function routingContext(input: {
10
11
  readonly address: string;
@@ -5,11 +5,11 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import "../group-routing/router.mjs";
8
- import { agentMessage } from "../group-routing/observed.mjs";
8
+ import { agentMessage } from "../group-routing/messages.mjs";
9
9
  import { archivedRoutingMessages } from "../group-routing/routing-context.mjs";
10
- import { isSilentReply } from "./silent.mjs";
11
10
  import { questionKey } from "../questions.mjs";
12
11
  import { questionsFromHistory, routingConfirmationState, routingConfirmations } from "./confirmations.mjs";
12
+ import { isSilentReply } from "./silent.mjs";
13
13
  const DELIVERY_CLOCK_TOLERANCE_MS = 6e4;
14
14
  async function routingContext(input) {
15
15
  const histories = new Map();
@@ -3,11 +3,9 @@ import type { ChannelSource } from "eve/channels";
3
3
  import type { TelegramChannelState } from "eve/channels/telegram";
4
4
  import type { SessionSettings } from "../../config.js";
5
5
  /**
6
- * Session policy for the Telegram channel (`agent-design.md`, section 4, and docs/agent.md,
7
- * "Sessions"): what a group session spans, the daily compaction and the idle reset. The policy
8
- * runs in the webhook route, before eve delivers a message, with the operations the route offers
9
- * on a continuation address (`resolveSession`, `compact`, `clear`); the history it needs is read
10
- * from the session's own durable stream, so nothing is stored beside eve.
6
+ * Telegram session addressing (`agent-design.md`, section 4, and docs/agent.md, "Sessions"): what
7
+ * a group session spans. Compaction and idle expiry are eve's own: automatic compaction near the
8
+ * context window and `limits.sessionTimeoutMs` in the workspace's `agent.ts`.
11
9
  */
12
10
  /** eve's Telegram continuation address: `chat:thread:conversation`, the last two possibly empty. */
13
11
  export interface TelegramAddress {
@@ -28,18 +26,7 @@ export declare function messageAddress(message: TelegramMessage): string;
28
26
  * eve's own address. A private chat has no anchor either way.
29
27
  */
30
28
  export declare function scopedAddress(address: string, scope: SessionSettings["scope"]): string;
31
- /** The most recent `HH:MM` in `timezone` at or before `now`. */
32
- export declare function compactBoundary(now: Date, compactAt: string, timezone: string): Date;
33
- /** What the policy knows about one session: from its stream, then kept per process. */
34
- export interface SessionHistory {
35
- /** When the session last did anything. */
36
- readonly lastAt: number;
37
- /** When its history last started afresh: creation, compaction or clear. */
38
- readonly resetAt: number;
39
- /** Turns completed since `resetAt`; a compaction is worth nothing before the first. */
40
- readonly turnsSinceReset: number;
41
- }
42
- /** One recorded event used by session policy, routing context and interaction projections. */
29
+ /** One recorded event used by routing context and interaction projections. */
43
30
  export interface RecordedEvent {
44
31
  readonly type: string;
45
32
  readonly scope?: {
@@ -53,18 +40,8 @@ export interface RecordedEvent {
53
40
  }
54
41
  /** Session events, oldest first; readers choose a tail or full stream for their consumers. */
55
42
  export type HistoryReader = (sessionId: string) => Promise<ReadonlyArray<RecordedEvent>>;
56
- /** How many events of the tail the reader fetches: enough for a day of a busy group. */
43
+ /** How many events of the tail the adapter's tail reader fetches. */
57
44
  export declare const HISTORY_TAIL = 400;
58
- /** Folds a tail into a history; with no reset marker in it, `resetAt` is its earliest event. */
59
- export declare function historyOf(events: ReadonlyArray<RecordedEvent>): SessionHistory;
60
- /**
61
- * The earliest message time a group turn's conversation evidence still needs: each member
62
- * message the session took in carried the chat's evidence up to then, and its history keeps it
63
- * until a compaction or clear. Undefined when the session has none since its last reset, so the
64
- * turn carries the whole window. Schedule sends and button answers carry no evidence and are
65
- * skipped; a workspace callback turn is indistinguishable and may hide earlier unseen chatter.
66
- */
67
- export declare function evidenceSince(events: ReadonlyArray<RecordedEvent>): number | undefined;
68
45
  /** The route's operations on a continuation address, as eve gives them to the handler. */
69
46
  export interface SessionOperations {
70
47
  readonly resolveSession: (address: string) => Promise<{
@@ -75,29 +52,5 @@ export interface SessionOperations {
75
52
  } | undefined>;
76
53
  readonly from: (address: string) => {
77
54
  readonly send?: ChannelSource<TelegramChannelState>["send"];
78
- compact: ChannelSource<TelegramChannelState>["compact"];
79
- clear: ChannelSource<TelegramChannelState>["clear"];
80
55
  };
81
56
  }
82
- export type SessionAction = "none" | "cleared" | "compacted";
83
- export interface SessionPolicy {
84
- /**
85
- * Applies the policy to the session `address` continues, before a message is delivered to it:
86
- * clears a history idle for longer than `idleResetHours`, else compacts it once after each
87
- * day's `compactAt`. A session that does not exist yet needs neither.
88
- */
89
- apply(input: {
90
- readonly address: string;
91
- readonly operations: SessionOperations;
92
- /** The newest activity the stream does not show, such as an observed group message. */
93
- readonly activeAt?: number;
94
- readonly now?: Date;
95
- }): Promise<SessionAction>;
96
- /** Records that a message was just delivered to the session. */
97
- delivered(address: string, now?: Date): void;
98
- }
99
- export declare function sessionPolicy(input: {
100
- readonly settings: () => SessionSettings;
101
- readonly read: HistoryReader;
102
- readonly log?: (message: string, error: unknown) => void;
103
- }): SessionPolicy;