@segmentstream/agent 0.6.13 → 0.7.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 (67) hide show
  1. package/README.md +41 -42
  2. package/dist/_chunks/{inbound-request-CysnjMdw.mjs → inbound-request-BwBvXk0A.mjs} +9 -2
  3. package/dist/extension/_manifest.json +2 -0
  4. package/dist/extension/channels/telegram.mjs +1 -0
  5. package/dist/extension/config.d.ts +17 -5
  6. package/dist/extension/config.mjs +26 -5
  7. package/dist/extension/connections/ontology.mjs +1 -1
  8. package/dist/extension/instructions.md +20 -0
  9. package/dist/extension/lib/attachment-formats.d.ts +1 -1
  10. package/dist/extension/lib/attachment-tables.d.ts +1 -1
  11. package/dist/extension/lib/confirmation-description.d.ts +5 -0
  12. package/dist/extension/lib/confirmation-description.mjs +7 -1
  13. package/dist/extension/lib/confirmation-outcome.d.ts +19 -0
  14. package/dist/extension/lib/confirmation-outcome.mjs +34 -0
  15. package/dist/extension/lib/confirmations.mjs +1 -1
  16. package/dist/extension/lib/connection-proxy.d.ts +2 -2
  17. package/dist/extension/lib/connection-proxy.mjs +6 -6
  18. package/dist/extension/lib/conversations.mjs +15 -1
  19. package/dist/extension/lib/customer-text-safety.mjs +2 -1
  20. package/dist/extension/lib/deferred-confirmations.d.ts +39 -0
  21. package/dist/extension/lib/deferred-confirmations.mjs +149 -0
  22. package/dist/extension/lib/ontology-tools.d.ts +2 -0
  23. package/dist/extension/lib/ontology-tools.mjs +5 -0
  24. package/dist/extension/lib/telegram/adapter.d.ts +6 -0
  25. package/dist/extension/lib/telegram/adapter.mjs +89 -14
  26. package/dist/extension/lib/telegram/attachment-description.d.ts +31 -0
  27. package/dist/extension/lib/telegram/attachment-description.mjs +82 -0
  28. package/dist/extension/lib/telegram/attachments-send.mjs +3 -3
  29. package/dist/extension/lib/telegram/audience.mjs +1 -1
  30. package/dist/extension/lib/telegram/bot.mjs +3 -1
  31. package/dist/extension/lib/telegram/chat-post-card.mjs +1 -1
  32. package/dist/extension/lib/telegram/confirmation-expiry.d.ts +4 -0
  33. package/dist/extension/lib/telegram/confirmation-expiry.mjs +50 -0
  34. package/dist/extension/lib/telegram/confirmations.d.ts +9 -1
  35. package/dist/extension/lib/telegram/confirmations.mjs +136 -15
  36. package/dist/extension/lib/telegram/conversations.d.ts +10 -1
  37. package/dist/extension/lib/telegram/conversations.mjs +177 -22
  38. package/dist/extension/lib/telegram/customer-text.mjs +1 -1
  39. package/dist/extension/lib/telegram/deferred-confirmations.d.ts +26 -0
  40. package/dist/extension/lib/telegram/deferred-confirmations.mjs +282 -0
  41. package/dist/extension/lib/telegram/files.mjs +20 -3
  42. package/dist/extension/lib/telegram/images.d.ts +1 -0
  43. package/dist/extension/lib/telegram/images.mjs +2 -1
  44. package/dist/extension/lib/telegram/inbound-request.d.ts +15 -1
  45. package/dist/extension/lib/telegram/inbound-request.mjs +1 -1
  46. package/dist/extension/lib/telegram/inbound.d.ts +12 -4
  47. package/dist/extension/lib/telegram/inbound.mjs +258 -74
  48. package/dist/extension/lib/telegram/media-policy.mjs +1 -1
  49. package/dist/extension/lib/telegram/observed.d.ts +45 -2
  50. package/dist/extension/lib/telegram/observed.mjs +62 -3
  51. package/dist/extension/lib/telegram/pdf-text.d.ts +5 -0
  52. package/dist/extension/lib/telegram/pdf-text.mjs +84 -0
  53. package/dist/extension/lib/telegram/router.d.ts +31 -7
  54. package/dist/extension/lib/telegram/router.mjs +152 -41
  55. package/dist/extension/lib/telegram/routing-context.d.ts +15 -3
  56. package/dist/extension/lib/telegram/routing-context.mjs +131 -6
  57. package/dist/extension/lib/telegram/silent.mjs +5 -3
  58. package/dist/extension/lib/telegram/turn-errors.mjs +1 -1
  59. package/dist/extension/lib/telegram/voice.d.ts +4 -0
  60. package/dist/extension/lib/telegram/voice.mjs +7 -6
  61. package/dist/extension/schedules/confirmations.d.ts +6 -0
  62. package/dist/extension/schedules/confirmations.mjs +33 -0
  63. package/dist/extension/tools/ontology.d.ts +4 -0
  64. package/dist/extension/tools/ontology.mjs +30 -0
  65. package/dist/tools/index.d.ts +1 -0
  66. package/dist/tools/index.mjs +2 -1
  67. package/package.json +7 -4
@@ -5,17 +5,20 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { memberIdOf, telegramPrincipal } from "./principal.mjs";
8
- import { chatPostOperationId, isChatPost } from "../chat-post-confirmation.mjs";
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";
10
11
  import { chatPostCard, chatPostCardStatus, chatPostReceipt, withChatPostCard } from "./chat-post-card.mjs";
11
12
  import { confirmations } from "../confirmations.mjs";
12
13
  import { conversationEvent } from "../conversations.mjs";
13
- import { n as currentInboundRequest, p as reportTelegramError } from "../../../_chunks/inbound-request-CysnjMdw.mjs";
14
- import { answerMarker, isPendingQuestion, pendingQuestions, questionKey } from "../questions.mjs";
14
+ import { n as currentInboundRequest, p as reportTelegramError } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
15
15
  import { formatAddress, scopedAddress } from "./sessions.mjs";
16
+ import { agentMessage, observedChatKey, withConfirmationPrompt } from "./observed.mjs";
17
+ import { postDeferredCard } from "./deferred-confirmations.mjs";
18
+ import { answerMarker, isPendingQuestion, pendingQuestions, questionKey } from "../questions.mjs";
16
19
  import { telegramTextLinks } from "@segmentstream-cloud/ontology/telegram";
17
- import { registerTelegramFreeformPrompt, renderTelegramInputRequest } from "eve/channels/telegram";
18
20
  import { createHash } from "node:crypto";
21
+ import { registerTelegramFreeformPrompt, renderTelegramInputRequest } from "eve/channels/telegram";
19
22
  const PREFIX = "ss-confirm:";
20
23
  function shortHash(value) {
21
24
  return createHash("sha256").update(value).digest("hex").slice(0, 20);
@@ -31,7 +34,7 @@ function openConfirmations(events) {
31
34
  const open = new Map();
32
35
  for (const event of events) {
33
36
  const data = object(event.data);
34
- if (event.type === "context.cleared") open.clear();
37
+ if (event.type === "context.cleared" || event.type === "session.started") open.clear();
35
38
  if (event.type === "input.requested" && Array.isArray(data.requests)) for (const item of data.requests) {
36
39
  const request = object(item);
37
40
  const action = object(request.action);
@@ -47,10 +50,55 @@ function openConfirmations(events) {
47
50
  const id = object(item).requestId;
48
51
  if (typeof id === "string") open.delete(id);
49
52
  }
53
+ if (event.type === "action.result") {
54
+ for (const [key, prompt] of open) if (prompt.callId === object(data.result).callId) open.delete(key);
55
+ }
50
56
  }
51
57
  return [...open.values()];
52
58
  }
59
+ function routingConfirmationState(events, prompt, at) {
60
+ if (prompt.state !== void 0 && prompt.state !== "pending" && prompt.state !== "approved") return prompt.state;
61
+ let state = prompt.state ?? "pending";
62
+ let callId;
63
+ for (const event of prompt.mode === "deferred" ? [] : events) {
64
+ const data = object(event.data);
65
+ if ((event.type === "context.cleared" || event.type === "session.started") && Date.parse(event.meta.at) >= Date.parse(prompt.at)) state = "expired";
66
+ if (event.type === "input.requested" && Array.isArray(data.requests)) {
67
+ for (const request of data.requests) if (object(request).requestId === prompt.requestId) {
68
+ callId = String(object(object(request).action).callId);
69
+ state = prompt.state ?? "pending";
70
+ }
71
+ }
72
+ if (event.type === "input.resolved" && Array.isArray(data.resolutions)) {
73
+ for (const resolution of data.resolutions) if (object(resolution).requestId === prompt.requestId) state = object(resolution).outcome === "approved" ? "approved" : "cancelled";
74
+ }
75
+ if (event.type === "action.result" && callId !== void 0 && object(data.result).callId === callId) state = data.status === "completed" ? "completed" : "failed";
76
+ }
77
+ if (state === "pending" && Date.parse(at) - Date.parse(prompt.at) >= 6e5) return "expired";
78
+ return state;
79
+ }
80
+ function routingConfirmations(events, recent, at) {
81
+ return openConfirmations(events).filter((prompt) => {
82
+ const age = Date.parse(at) - prompt.at;
83
+ const delivered = recent.findLast((message) => "role" in message && message.confirmation?.requestId === prompt.requestId);
84
+ const state = delivered !== void 0 && "role" in delivered ? delivered.confirmation?.state : void 0;
85
+ return age >= 0 && age < 6e5 && (state === void 0 || state === "pending");
86
+ }).slice(-5).map((prompt) => {
87
+ const delivered = recent.findLast((message) => "role" in message && message.confirmation?.requestId === prompt.requestId);
88
+ const summary = delivered !== void 0 && "role" in delivered ? delivered.confirmation : void 0;
89
+ return {
90
+ state: "pending",
91
+ action: summary?.action ?? "the requested action",
92
+ requestedBy: summary?.requestedBy ?? null,
93
+ at: summary?.at ?? new Date(prompt.at).toISOString()
94
+ };
95
+ });
96
+ }
53
97
  function questionFromHistory(events, owner) {
98
+ const pending = questionsFromHistory(events);
99
+ return owner === void 0 ? pending.at(-1) : pending.find((question) => questionKey(question) === questionKey(owner));
100
+ }
101
+ function questionsFromHistory(events) {
54
102
  const pending = new Map();
55
103
  for (const event of events) {
56
104
  const data = object(event.data);
@@ -64,7 +112,7 @@ function questionFromHistory(events, owner) {
64
112
  for (const [key, question] of pending) if (data.message.startsWith(answerMarker(question.id))) pending.delete(key);
65
113
  }
66
114
  }
67
- return owner === void 0 ? [...pending.values()].at(-1) : pending.get(questionKey(owner));
115
+ return [...pending.values()];
68
116
  }
69
117
  function questionAddress(owner) {
70
118
  return formatAddress({
@@ -136,7 +184,31 @@ async function handleConfirmationCallback(input) {
136
184
  }, edit ? "The chat post was cancelled for editing. Ask the member what to change using questions_ask, then make a NEW chat_post_message call and confirmation. Never reuse the cancelled call." : void 0);
137
185
  await notice(expired ? "⌛" : void 0);
138
186
  }
139
- function telegramConfirmationEvents(archive) {
187
+ function messagePrompt(message) {
188
+ return message.prompt ?? {
189
+ requestId: message.requestId,
190
+ action: "the requested action",
191
+ requestedBy: null,
192
+ at: new Date().toISOString()
193
+ };
194
+ }
195
+ function telegramConfirmationEvents(archive, observed, deferredOptions) {
196
+ const remember = (channel, ctx, messageId, text, confirmation) => {
197
+ const principal = ctx.session.auth.current;
198
+ const name = humanText(principal?.attributes.memberName);
199
+ observed?.remember(observedChatKey(channel.telegram), {
200
+ ...agentMessage({
201
+ text,
202
+ at: new Date().toISOString(),
203
+ messageId,
204
+ ...principal?.subject === void 0 || name === void 0 ? {} : { inReplyTo: {
205
+ id: principal.subject,
206
+ author: name
207
+ } }
208
+ }),
209
+ ...confirmation === void 0 ? {} : { confirmation }
210
+ });
211
+ };
140
212
  const record = async (callId, phase, text, status, ctx) => {
141
213
  const saved = confirmations.get()[callId];
142
214
  if (archive === void 0 || saved?.description?.chatPost === void 0 || saved.source === void 0) return;
@@ -184,6 +256,7 @@ function telegramConfirmationEvents(archive) {
184
256
  text: rendered.text,
185
257
  reply_markup: rendered.replyMarkup
186
258
  });
259
+ remember(channel, ctx, sent.id, rendered.text);
187
260
  if (rendered.freeformRequestId !== void 0) registerTelegramFreeformPrompt(channel.state, {
188
261
  messageId: sent.id,
189
262
  requestId: request.requestId
@@ -228,7 +301,30 @@ function telegramConfirmationEvents(archive) {
228
301
  }
229
302
  ]] }
230
303
  });
231
- const sent = await (exactPost ? withChatPostCard(text, send, copy.previewLinks) : withConfirmationText(text, send));
304
+ const prompt = {
305
+ requestId: request.requestId,
306
+ state: "pending",
307
+ sessionId: ctx.session.id,
308
+ address: channel.continuation?.token ?? formatAddress({
309
+ chatId: channel.telegram.chatId,
310
+ messageThreadId: String(channel.telegram.messageThreadId ?? ""),
311
+ conversationId: channel.telegram.conversationId ?? ""
312
+ }),
313
+ action: humanText(description?.action) ?? "the requested action",
314
+ requestedBy: humanText(ctx.session.auth.current?.attributes.memberName) ?? null,
315
+ at: new Date().toISOString(),
316
+ ...ctx.session.auth.current?.subject === void 0 ? {} : { requester: {
317
+ id: ctx.session.auth.current.subject,
318
+ author: humanText(ctx.session.auth.current.attributes.memberName) ?? "the requester"
319
+ } }
320
+ };
321
+ const sent = await withConfirmationPrompt(prompt, () => exactPost ? withChatPostCard(text, send, copy.previewLinks) : withConfirmationText(text, send));
322
+ remember(channel, ctx, sent.id, text, prompt);
323
+ channel.continuation?.alias(formatAddress({
324
+ chatId: channel.telegram.chatId,
325
+ messageThreadId: String(channel.telegram.messageThreadId ?? ""),
326
+ conversationId: ""
327
+ }));
232
328
  const state = channel.state;
233
329
  state.confirmationMessages = {
234
330
  ...state.confirmationMessages,
@@ -238,6 +334,7 @@ function telegramConfirmationEvents(archive) {
238
334
  text,
239
335
  copy,
240
336
  exactPost,
337
+ prompt,
241
338
  previewLinks: copy.previewLinks
242
339
  }
243
340
  };
@@ -271,7 +368,13 @@ function telegramConfirmationEvents(archive) {
271
368
  replyMarkup: { inline_keyboard: [] },
272
369
  text
273
370
  });
274
- await (message.exactPost ? withChatPostCard(text, edit, [...message.previewLinks ?? [], ...telegramTextLinks(status)]) : withConfirmationText(text, edit));
371
+ const prompt = {
372
+ ...messagePrompt(message),
373
+ state: resolution.outcome === "approved" ? "approved" : reason === "expired" ? "expired" : "cancelled"
374
+ };
375
+ message.prompt = prompt;
376
+ await withConfirmationPrompt(prompt, () => message.exactPost ? withChatPostCard(text, edit, [...message.previewLinks ?? [], ...telegramTextLinks(status)]) : withConfirmationText(text, edit));
377
+ remember(channel, ctx, message.messageId, text, prompt);
275
378
  await record(callId, reason ?? resolution.outcome, text, resolution.outcome === "approved" ? "requested" : "cancelled", ctx);
276
379
  if (resolution.outcome !== "approved") {
277
380
  delete state.confirmationMessages?.[callId];
@@ -287,6 +390,13 @@ function telegramConfirmationEvents(archive) {
287
390
  },
288
391
  async "action.result"(data, channel, ctx) {
289
392
  const state = channel.state;
393
+ if (data.result.kind === "tool-result" && deferredOptions !== void 0 && archive !== void 0) {
394
+ const output = object(data.result.output);
395
+ if (output.status === "awaiting_confirmation" && typeof output.confirmation === "string") {
396
+ await postDeferredCard(output.confirmation, channel, ctx, deferredOptions, archive, observed);
397
+ return;
398
+ }
399
+ }
290
400
  if (data.result.kind === "tool-result") {
291
401
  const question = object(data.result.output).pendingQuestion;
292
402
  if (data.result.toolName.endsWith("__questions_ask") && isPendingQuestion(question)) {
@@ -313,13 +423,14 @@ function telegramConfirmationEvents(archive) {
313
423
  conversationId: ""
314
424
  }));
315
425
  channel.continuation?.alias(questionAddress(saved));
316
- await channel.telegram.post({
426
+ const sent = await channel.telegram.post({
317
427
  text: question.question,
318
428
  reply_markup: {
319
429
  force_reply: true,
320
430
  selective: true
321
431
  }
322
432
  });
433
+ remember(channel, ctx, sent.id, question.question);
323
434
  }
324
435
  }
325
436
  const message = state.confirmationMessages?.[data.result.callId];
@@ -331,22 +442,32 @@ function telegramConfirmationEvents(archive) {
331
442
  const errorId = failed ? reportTelegramError("confirmed_chat_post_failed", data, { callId: data.result.callId }) : void 0;
332
443
  const status = receipt ?? (failed ? `${copy.failed} ${errorId}\n⚠️ ❓` : "⚠️ ❓");
333
444
  const text = chatPostCardStatus(message.text, status);
334
- await withChatPostCard(text, () => channel.telegram.editMessageText({
445
+ const prompt = {
446
+ ...messagePrompt(message),
447
+ state: failed ? "failed" : "completed"
448
+ };
449
+ await withConfirmationPrompt(prompt, () => withChatPostCard(text, () => channel.telegram.editMessageText({
335
450
  messageId: message.messageId,
336
451
  replyMarkup: { inline_keyboard: [] },
337
452
  text
338
- }), [...message.previewLinks ?? [], ...telegramTextLinks(status)]);
453
+ }), [...message.previewLinks ?? [], ...telegramTextLinks(status)]));
454
+ remember(channel, ctx, message.messageId, text, prompt);
339
455
  await record(data.result.callId, "receipt", text, failed ? "failed" : object(data.result.kind === "tool-result" ? data.result.output : void 0).status === "sent" ? "completed" : "unknown", ctx);
340
456
  } else {
341
457
  const copy = message.copy ?? renderConfirmation();
342
458
  const errorId = failed ? reportTelegramError("confirmed_action_failed", data, { callId: data.result.callId }) : void 0;
343
459
  const status = failed ? `${copy.failed} ${errorId}` : copy.completed;
344
460
  const text = `${message.text}\n\n${status}`;
345
- await withConfirmationText(text, () => channel.telegram.editMessageText({
461
+ const prompt = {
462
+ ...messagePrompt(message),
463
+ state: failed ? "failed" : "completed"
464
+ };
465
+ await withConfirmationPrompt(prompt, () => withConfirmationText(text, () => channel.telegram.editMessageText({
346
466
  messageId: message.messageId,
347
467
  replyMarkup: { inline_keyboard: [] },
348
468
  text
349
- }));
469
+ })));
470
+ remember(channel, ctx, message.messageId, text, prompt);
350
471
  }
351
472
  delete state.confirmationMessages?.[data.result.callId];
352
473
  },
@@ -368,4 +489,4 @@ function telegramConfirmationEvents(archive) {
368
489
  };
369
490
  }
370
491
  const confirmationEvents = telegramConfirmationEvents();
371
- export { confirmationCallback, confirmationEvents, handleConfirmationCallback, questionAddress, questionFromHistory, telegramConfirmationEvents };
492
+ export { confirmationCallback, confirmationEvents, handleConfirmationCallback, questionAddress, questionFromHistory, questionsFromHistory, routingConfirmationState, routingConfirmations, telegramConfirmationEvents };
@@ -2,7 +2,10 @@ import type { AgentIdentities } from "../identities.js";
2
2
  import { type Conversations } from "../conversations.js";
3
3
  import type { AgentChats } from "./chats.js";
4
4
  import type { ObservedMessages } from "./observed.js";
5
+ import type { DecodedAttachment } from "./attachment-description.js";
5
6
  import type { AddressingReason, RouteDecider, RouteDecision } from "./router.js";
7
+ /** A crashed routing attempt can be reclaimed; the per-message turn claim still fences dispatch. */
8
+ export declare const REVISION_CLAIM_TTL_MS: number;
6
9
  /** Runs after webhook verification, before eve drops bot/channel/service updates. */
7
10
  export declare function captureTelegramWebhook(input: {
8
11
  readonly request: Request;
@@ -14,8 +17,14 @@ export declare function captureTelegramWebhook(input: {
14
17
  }): Promise<Response>;
15
18
  /** Additive inbound hook: the router keeps ownership of the reply policy. */
16
19
  export declare function recordGateDecision(reason: string, decision?: "reply" | "silent"): void;
17
- export declare function recordRouteDecision(route: "reply" | "observe", addressed: boolean | AddressingReason | undefined, policy: string, audit?: Pick<RouteDecision, "reason" | "note">): void;
20
+ export declare function recordRouteDecision(route: "reply" | "observe", addressed: boolean | AddressingReason | undefined, policy: string, audit?: Pick<RouteDecision, "reason" | "note" | "diagnostics">): void;
18
21
  export declare function captureRouteDecider(decide: RouteDecider): RouteDecider;
22
+ /** Update the existing inbound event after STT/description, under the same text retention policy. */
23
+ export declare function recordInboundContent(content: {
24
+ readonly text: string;
25
+ readonly media?: string;
26
+ readonly decoded?: ReadonlyArray<DecodedAttachment>;
27
+ }): Promise<void>;
19
28
  /** Captures message sends and button/status edits, including pairing and proactive replies. */
20
29
  export declare function captureTelegramFetch(fetchImpl: typeof fetch, archive: Conversations, identities: () => AgentIdentities): typeof fetch;
21
30
  /** Reuses the request's verified lookup; it never survives into another inbound request. */
@@ -4,16 +4,18 @@ 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 { collapseConversationEvents, conversationEvent } from "../conversations.mjs";
8
- import { observedChatKey } from "./observed.mjs";
9
- import { n as currentInboundRequest, s as currentThreadId } from "../../../_chunks/inbound-request-CysnjMdw.mjs";
10
- import { reportMembership } from "./membership.mjs";
7
+ import { conversationEvent } from "../conversations.mjs";
8
+ import { n as currentInboundRequest, s as currentThreadId } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
11
9
  import { contentShape, messageContent } from "./content.mjs";
12
- import { parseTelegramUpdate } from "eve/channels/telegram";
10
+ import { currentConfirmationPrompt, messageMetadata, observedChatKey } from "./observed.mjs";
11
+ import { archivedRoutingMessages } from "./routing-context.mjs";
12
+ import { reportMembership } from "./membership.mjs";
13
13
  import { createHash, randomUUID } from "node:crypto";
14
+ import { parseTelegramUpdate } from "eve/channels/telegram";
14
15
  const captures = new WeakMap();
15
16
  const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
16
17
  const id = (value) => typeof value === "number" || typeof value === "string" ? String(value) : null;
18
+ const REVISION_CLAIM_TTL_MS = 12e4;
17
19
  function attachmentRefs(message) {
18
20
  const files = message.attachments.map((file) => ({
19
21
  id: file.fileId,
@@ -44,6 +46,16 @@ function attachmentRefs(message) {
44
46
  }
45
47
  return files;
46
48
  }
49
+ function attachmentIdentity(files) {
50
+ return JSON.stringify(files.map((file) => [file.kind, file.uniqueId ?? file.id]));
51
+ }
52
+ function eventRouting(event) {
53
+ try {
54
+ return record(record(JSON.parse(event.argsSummary ?? "{}")).routing);
55
+ } catch {
56
+ return {};
57
+ }
58
+ }
47
59
  async function participants(message, identities, resolved) {
48
60
  if (message.chat.type !== "private" || message.from === void 0 || message.from.isBot) return [];
49
61
  try {
@@ -85,6 +97,7 @@ async function captureTelegramWebhook(input) {
85
97
  const time = edited === void 0 ? message.raw.date : message.raw.edit_date ?? message.raw.date;
86
98
  const sentAt = typeof time === "number" ? time * 1e3 : Date.now();
87
99
  const senderChat = record(message.raw.sender_chat);
100
+ const attachments = attachmentRefs(message);
88
101
  let lookup;
89
102
  const event = conversationEvent({
90
103
  eventId: [
@@ -99,7 +112,13 @@ async function captureTelegramWebhook(input) {
99
112
  threadId: message.messageThreadId === void 0 ? null : String(message.messageThreadId),
100
113
  kind: "inbound",
101
114
  text: content.text || null,
102
- argsSummary: JSON.stringify(contentShape(message.raw, content)),
115
+ argsSummary: JSON.stringify({
116
+ ...contentShape(message.raw, content),
117
+ routing: {
118
+ ...messageMetadata(message),
119
+ ...attachments.length === 0 ? {} : { originalText: content.text }
120
+ }
121
+ }),
103
122
  messageId: message.messageId,
104
123
  replyToMessageId: message.replyToMessage?.messageId ?? null,
105
124
  senderId: id(senderChat.id) ?? message.from?.id ?? null,
@@ -108,15 +127,33 @@ async function captureTelegramWebhook(input) {
108
127
  participantIds: await participants(message, input.identities, (result) => {
109
128
  lookup = result;
110
129
  }),
111
- attachments: attachmentRefs(message),
130
+ attachments,
112
131
  at: new Date(sentAt).toISOString()
113
132
  });
114
133
  await input.archive.append(event);
115
134
  const capture = {
116
135
  event,
117
136
  message,
137
+ archive: input.archive,
118
138
  ...lookup === void 0 ? {} : { lookup }
119
139
  };
140
+ const revisionId = edited === void 0 ? void 0 : `telegram:${message.chat.id}:${message.messageId}:routing:${createHash("sha256").update(JSON.stringify({
141
+ at: event.at,
142
+ content,
143
+ ...event.attachments.length === 0 ? {} : { attachments: attachmentIdentity(event.attachments) }
144
+ })).digest("hex")}`;
145
+ const revisionEvents = [];
146
+ let revisionClaim;
147
+ const finishRevision = async (status) => {
148
+ if (revisionClaim === void 0) return;
149
+ await input.archive.append({
150
+ ...revisionClaim,
151
+ eventId: status === "completed" ? `${revisionId}:completed` : `${revisionClaim.eventId}:failed`,
152
+ at: new Date(Date.now()).toISOString(),
153
+ status,
154
+ reason: status === "completed" ? "routing_completed" : "routing_failed"
155
+ });
156
+ };
120
157
  const inbound = currentInboundRequest();
121
158
  if (inbound !== void 0) captures.set(inbound, capture);
122
159
  const chat = observedChatKey({
@@ -132,37 +169,88 @@ async function captureTelegramWebhook(input) {
132
169
  includeDecisions: true,
133
170
  ...lookup?.status === "paired" ? { participantId: lookup.member.id } : {}
134
171
  });
135
- const routing = automated ? [] : (await input.archive.context({
172
+ const messageHistory = automated ? [] : (await input.archive.context({
136
173
  provider: "telegram",
137
174
  chatId: message.chat.id,
138
175
  messageId: message.messageId,
139
176
  includeDecisions: true,
140
177
  limit: 100,
141
178
  ...lookup?.status === "paired" ? { participantId: lookup.member.id } : {}
142
- })).events.filter((earlier) => earlier.kind === "decision" && earlier.senderId === event.senderId);
179
+ })).events;
180
+ const routing = messageHistory.filter((earlier) => earlier.kind === "decision" && earlier.senderId === event.senderId);
143
181
  const previousClaim = routing.find((earlier) => earlier.reason === "turn_claimed");
144
182
  const admitted = routing.some((earlier) => earlier.reason === "turn_admitted");
145
183
  const released = previousClaim !== void 0 && routing.some((earlier) => earlier.eventId === `${previousClaim.eventId}:released`);
146
184
  const dispatched = routing.some((earlier) => earlier.reason === "turn_dispatch_started");
147
185
  const reserved = previousClaim !== void 0 && !released && Date.now() - Date.parse(previousClaim.at) < 6e5;
148
186
  const alreadyHandled = admitted || dispatched || reserved;
149
- if (!automated && edited === void 0 && !alreadyHandled) for (const earlier of (input.observed.recent(chat).length === 0 ? collapseConversationEvents(history?.events ?? []) : []).filter((item) => item.kind === "inbound" && item.eventId !== event.eventId && !history?.events.some((decision) => decision.kind === "decision" && decision.messageId === item.messageId && decision.reason === "turn_admitted")).slice(0, 30).reverse()) input.observed.add(chat, {
150
- author: earlier.senderName ?? earlier.senderId ?? "automated post",
151
- text: earlier.text ?? "[attachment or service message]",
152
- at: earlier.at,
153
- ...earlier.messageId === null ? {} : { messageId: earlier.messageId },
154
- ...earlier.senderId === null ? {} : { authorId: earlier.senderId }
155
- });
156
- else if (automated || edited !== void 0) {
187
+ const rememberRevision = (events) => {
188
+ if (revisionId !== void 0) revisionEvents.push(...events.filter((item) => item.eventId === revisionId || item.eventId.startsWith(`${revisionId}:`)));
189
+ };
190
+ const historyEvents = [...messageHistory];
191
+ rememberRevision(historyEvents);
192
+ const previousRevision = (earlier) => earlier.kind === "inbound" && earlier.messageId === message.messageId && earlier.senderId === event.senderId && earlier.eventId !== event.eventId && !(edited !== void 0 && earlier.eventId.includes(":edit:") && earlier.at === event.at && (eventRouting(earlier).originalText ?? earlier.text) === content.text && attachmentIdentity(earlier.attachments) === attachmentIdentity(event.attachments));
193
+ const previous = messageHistory.find(previousRevision);
194
+ const unchangedAttachments = previous !== void 0 && attachmentIdentity(previous.attachments) === attachmentIdentity(event.attachments);
195
+ const previousRouting = previous === void 0 ? {} : eventRouting(previous);
196
+ const originalEvent = previous === void 0 ? void 0 : historyEvents.find((item) => item.eventId === previous.eventId.split(":content:")[0]);
197
+ if (inbound !== void 0) {
198
+ inbound.previousText = typeof previousRouting.originalText === "string" ? previousRouting.originalText : originalEvent?.text ?? previous?.text ?? void 0;
199
+ inbound.attachmentsChanged = previous === void 0 ? event.attachments.length > 0 : !unchangedAttachments;
200
+ if (unchangedAttachments && event.attachments.length > 0) {
201
+ const decoded = Array.isArray(previousRouting.decoded) ? previousRouting.decoded : [];
202
+ const media = typeof previousRouting.media === "string" ? previousRouting.media : void 0;
203
+ const metadata = record(JSON.parse(capture.event.argsSummary ?? "{}"));
204
+ capture.event = {
205
+ ...capture.event,
206
+ argsSummary: JSON.stringify({
207
+ ...metadata,
208
+ routing: {
209
+ ...record(metadata.routing),
210
+ membership: previousRouting.membership
211
+ }
212
+ })
213
+ };
214
+ inbound.previousContent = {
215
+ decoded,
216
+ ...media === void 0 ? {} : { media }
217
+ };
218
+ const transcripts = decoded.flatMap((item) => [
219
+ "voice",
220
+ "audio",
221
+ "video_note"
222
+ ].includes(item.attachment_kind) && item.processed && typeof item.content === "string" ? [item.content] : []);
223
+ await recordInboundContent({
224
+ text: [content.text, ...transcripts].filter(Boolean).join("\n\n"),
225
+ decoded,
226
+ ...media === void 0 ? {} : { media }
227
+ });
228
+ }
229
+ }
230
+ if (!automated) for (const earlier of input.observed.recent(chat).length === 0 ? archivedRoutingMessages((history?.events ?? []).filter((item) => item.eventId !== event.eventId)) : []) if ("role" in earlier || history?.events.some((decision) => decision.kind === "decision" && decision.messageId === earlier.messageId && decision.reason === "turn_admitted")) input.observed.remember(chat, earlier);
231
+ else input.observed.add(chat, earlier);
232
+ if (automated || edited !== void 0) {
157
233
  if (message.chat.type === "group" || message.chat.type === "supergroup") await input.chats?.seen({
158
234
  chatId: message.chat.id,
159
235
  title: message.chat.title ?? ""
160
236
  });
161
- input.observed.add(chat, {
237
+ const remembered = input.observed.recent(chat).findLast((entry) => entry.messageId === message.messageId);
238
+ const retained = alreadyHandled && edited === void 0 ? remembered : void 0;
239
+ const retainedContent = archivedRoutingMessages([{
240
+ ...capture.event,
241
+ sequence: "1",
242
+ storedAt: event.at
243
+ }])[0];
244
+ input.observed.add(chat, retained ?? (retainedContent === void 0 ? void 0 : {
245
+ ...retainedContent,
246
+ ...unchangedAttachments && remembered?.images !== void 0 ? { images: remembered.images } : {}
247
+ }) ?? {
248
+ ...messageMetadata(message),
162
249
  author: event.senderName ?? event.senderId ?? "automated post",
163
250
  text: event.text ?? "[attachment or service message]",
164
251
  at: event.at,
165
- messageId: message.messageId
252
+ messageId: message.messageId,
253
+ ...event.senderId === null ? {} : { authorId: event.senderId }
166
254
  });
167
255
  capture.decision = "silent";
168
256
  capture.reason = automated ? "automated_or_channel_message" : alreadyHandled ? "message_already_handled" : "message_edit";
@@ -174,6 +262,27 @@ async function captureTelegramWebhook(input) {
174
262
  }
175
263
  if (inbound !== void 0) {
176
264
  inbound.edited = edited !== void 0;
265
+ if (revisionId !== void 0) inbound.claimRevision = async () => {
266
+ if (revisionEvents.some((item) => item.eventId === `${revisionId}:completed`)) return false;
267
+ const claims = revisionEvents.filter((item) => item.reason === "routing_claimed");
268
+ const latest = claims[0];
269
+ if (latest !== void 0 && !revisionEvents.some((item) => item.eventId === `${latest.eventId}:failed`) && Date.now() - Date.parse(latest.at) < 12e4) return false;
270
+ const attempt = claims.length === 0 ? void 0 : Math.max(...claims.map((item) => Number(item.eventId.slice(revisionId.length).replace(":retry:", "")))) + 1;
271
+ const claim = {
272
+ ...event,
273
+ eventId: attempt === void 0 ? revisionId : `${revisionId}:retry:${attempt}`,
274
+ at: new Date(Date.now()).toISOString(),
275
+ kind: "decision",
276
+ decision: "silent",
277
+ reason: "routing_claimed",
278
+ status: "requested",
279
+ text: null,
280
+ attachments: []
281
+ };
282
+ const inserted = await input.archive.append(claim);
283
+ if (inserted !== false) revisionClaim = claim;
284
+ return inserted !== false;
285
+ };
177
286
  const nextClaim = previousClaim === void 0 ? void 0 : createHash("sha256").update(previousClaim.eventId).digest("hex");
178
287
  const claimId = previousClaim === void 0 ? `telegram:${message.chat.id}:${message.messageId}:turn` : `telegram:${message.chat.id}:${message.messageId}:turn:${nextClaim}`;
179
288
  let claimed = false;
@@ -232,6 +341,7 @@ async function captureTelegramWebhook(input) {
232
341
  reason: "inbound_handler_failed",
233
342
  attachments: []
234
343
  });
344
+ await finishRevision("failed");
235
345
  } catch (storageError) {
236
346
  console.error("[segmentstream] recording inbound failure failed", {
237
347
  storageError,
@@ -245,12 +355,16 @@ async function captureTelegramWebhook(input) {
245
355
  ...event,
246
356
  eventId: `${event.eventId}:decision`,
247
357
  kind: "decision",
248
- argsSummary: null,
358
+ argsSummary: JSON.stringify({
359
+ ...capture.diagnostics === void 0 ? {} : { decision: capture.diagnostics },
360
+ routing: { membership: capture.lookup === void 0 ? "unknown" : capture.lookup.status === "paired" ? "linked" : "unlinked" }
361
+ }),
249
362
  text: capture.note ?? (edited !== void 0 ? "Message edit recorded as context; no agent turn started." : null),
250
363
  decision: capture.decision ?? "silent",
251
364
  reason: capture.reason ?? (message.raw.left_chat_member ? "member_departure" : message.raw.migrate_to_chat_id || message.raw.migrate_from_chat_id ? "chat_migration" : "unpaired_refused_or_empty_message"),
252
365
  attachments: []
253
366
  });
367
+ await finishRevision(capture.reason === "inbound_message_failed" || capture.reason === "decision_model_failed" || capture.reason === "decision_model_timeout" ? "failed" : "completed");
254
368
  return response;
255
369
  }
256
370
  function recordGateDecision(reason, decision = "silent") {
@@ -269,6 +383,7 @@ function recordRouteDecision(route, addressed, policy, audit) {
269
383
  capture.decision = route === "reply" ? "reply" : "silent";
270
384
  capture.reason ??= audit?.reason ?? (typeof addressed === "string" ? addressed : addressed ? "addressed_message" : policy === "mention" ? "mention_policy" : `decision_model_${route}`);
271
385
  capture.note = audit?.note.slice(0, 500);
386
+ capture.diagnostics = audit?.diagnostics;
272
387
  }
273
388
  }
274
389
  function captureRouteDecider(decide) {
@@ -283,6 +398,34 @@ function captureRouteDecider(decide) {
283
398
  }
284
399
  };
285
400
  }
401
+ async function recordInboundContent(content) {
402
+ const inbound = currentInboundRequest();
403
+ const capture = inbound === void 0 ? void 0 : captures.get(inbound);
404
+ if (capture?.archive === void 0) return;
405
+ const metadata = record(JSON.parse(capture.event.argsSummary ?? "{}"));
406
+ const membership = capture.lookup === void 0 ? record(metadata.routing).membership : capture.lookup.status === "paired" ? "linked" : "unlinked";
407
+ 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;
408
+ const contentHash = createHash("sha256").update(JSON.stringify({
409
+ ...content,
410
+ membership
411
+ })).digest("hex").slice(0, 16);
412
+ const event = {
413
+ ...capture.event,
414
+ eventId: `${capture.event.eventId}:content:${contentHash}`,
415
+ text: content.text,
416
+ argsSummary: JSON.stringify({
417
+ ...metadata,
418
+ routing: {
419
+ ...record(metadata.routing),
420
+ membership,
421
+ media: content.media?.slice(0, 8e3),
422
+ decoded: content.decoded
423
+ }
424
+ })
425
+ };
426
+ await capture.archive.append(event);
427
+ capture.event = event;
428
+ }
286
429
  function captureTelegramFetch(fetchImpl, archive, identities) {
287
430
  return async (input, init) => {
288
431
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
@@ -316,6 +459,15 @@ function captureTelegramFetch(fetchImpl, archive, identities) {
316
459
  senderIsBot: true,
317
460
  senderId: "agent",
318
461
  senderName: "agent",
462
+ argsSummary: JSON.stringify({ routing: {
463
+ ...currentConfirmationPrompt() === void 0 ? {} : { confirmation: currentConfirmationPrompt() },
464
+ ...capture?.event.chatId === chatId && capture.event.senderId !== null ? { inReplyTo: {
465
+ id: capture.event.senderId,
466
+ author: capture.event.senderName ?? capture.event.senderId,
467
+ ...capture.message.from?.username === void 0 ? {} : { username: capture.message.from.username }
468
+ } } : {},
469
+ ...currentConfirmationPrompt()?.requester === void 0 ? {} : { inReplyTo: currentConfirmationPrompt()?.requester }
470
+ } }),
319
471
  participantIds,
320
472
  threadId: id(body.message_thread_id) ?? currentThreadId() ?? null,
321
473
  replyToMessageId: id(body.reply_to_message_id) ?? id(record(body.reply_parameters).message_id),
@@ -358,7 +510,10 @@ function conversationIdentities(identities) {
358
510
  lookup: async (account) => {
359
511
  const inbound = currentInboundRequest();
360
512
  const capture = inbound === void 0 ? void 0 : captures.get(inbound);
361
- return account.provider === "telegram" && capture?.message.from?.id === account.externalId && capture.lookup !== void 0 ? capture.lookup : identities.lookup(account);
513
+ if (account.provider === "telegram" && capture?.message.from?.id === account.externalId && capture.lookup !== void 0) return capture.lookup;
514
+ const lookup = await identities.lookup(account);
515
+ if (account.provider === "telegram" && capture?.message.from?.id === account.externalId) capture.lookup = lookup;
516
+ return lookup;
362
517
  }
363
518
  };
364
519
  }
@@ -394,4 +549,4 @@ function telegramConversationCapture(input) {
394
549
  }
395
550
  };
396
551
  }
397
- export { captureRouteDecider, captureTelegramFetch, captureTelegramWebhook, conversationIdentities, recordGateDecision, recordRouteDecision, telegramConversationCapture };
552
+ export { REVISION_CLAIM_TTL_MS, captureRouteDecider, captureTelegramFetch, captureTelegramWebhook, conversationIdentities, recordGateDecision, recordInboundContent, recordRouteDecision, telegramConversationCapture };
@@ -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-CysnjMdw.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";
9
9
  export { TelegramDeliveryError, currentLocale, currentThreadId, customerCopy, customerMessage, customerText, failureText, formattingRefused, isTechnicalText, reportTelegramError, safeTelegramFetch, withDeliveryLocale };
@@ -0,0 +1,26 @@
1
+ import type { TelegramCallbackQuery, TelegramContext, TelegramHandle, TelegramEventContext } from "eve/channels/telegram";
2
+ import type { SessionContext } from "eve/context";
3
+ import { type DeferredConfirmation, type DeferredOptions } from "../deferred-confirmations.js";
4
+ import { type Conversations } from "../conversations.js";
5
+ import type { AgentIdentities } from "../identities.js";
6
+ import { type ObservedMessages } from "./observed.js";
7
+ export declare const isDeferredCallback: (data?: string) => boolean;
8
+ export declare const deferredCallback: (record: DeferredConfirmation, choice: "y" | "n" | "e") => string;
9
+ export declare function archiveDeferred(record: DeferredConfirmation, phase: string, archive: Conversations): Promise<void>;
10
+ /** Called at the durable tool-result boundary. Pending cards never become eve input requests. */
11
+ export declare function postDeferredCard(id: string, channel: TelegramEventContext, ctx: SessionContext, options: DeferredOptions, archive: Conversations, observed?: ObservedMessages): Promise<void>;
12
+ /** Every callback re-resolves the live identity. The database arbitrates concurrent presses. */
13
+ export declare function handleDeferredCallback(input: {
14
+ readonly context: TelegramContext;
15
+ readonly query: TelegramCallbackQuery;
16
+ readonly identities: AgentIdentities;
17
+ readonly archive: Conversations;
18
+ readonly options: DeferredOptions;
19
+ readonly observed?: ObservedMessages;
20
+ }): Promise<void>;
21
+ /** A database-clock expiry claim is shared by webhooks and the minute schedule. */
22
+ export declare function expireDeferredRecord(record: DeferredConfirmation, context: {
23
+ readonly telegram: Pick<TelegramHandle, "editMessageText">;
24
+ }, options: DeferredOptions, archive: Conversations, notify: NonNullable<import("./inbound-request.js").InboundRequest["confirmationEvent"]>, observed?: ObservedMessages): Promise<void>;
25
+ /** Webhooks also reconcile expiry if the host's minute scheduler is delayed. */
26
+ export declare function expireDeferredCards(context: TelegramContext, options: DeferredOptions, archive: Conversations, observed?: ObservedMessages): Promise<void>;