@segmentstream/agent 0.28.0 → 0.29.1

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.
@@ -4,7 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
4
4
  const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
- var version = "0.28.0";
7
+ var version = "0.29.1";
8
8
  function agentBuild(environment = process.env) {
9
9
  return {
10
10
  agent: version,
@@ -29,5 +29,5 @@ export { agentMessage as groupAgentMessage, type RecentMessage, type AgentMessag
29
29
  export { archivedRoutingMessages } from "./lib/group-routing/routing-context.js";
30
30
  export { replayRouterPayload } from "./lib/group-routing/replay.js";
31
31
  export { conversationAddressing as groupConversationAddressing } from "./lib/group-routing/addressing.js";
32
- export { routingBurst, debounceBurst } from "./lib/group-routing/burst.js";
32
+ export { routingBurst } from "./lib/group-routing/burst.js";
33
33
  export type { TelegramTexts } from "./lib/telegram/texts.js";
@@ -13,11 +13,11 @@ import { decisionRouter, routeMessage } from "./lib/group-routing/router.mjs";
13
13
  import { agentMessage } from "./lib/group-routing/messages.mjs";
14
14
  import { archivedRoutingMessages } from "./lib/group-routing/routing-context.mjs";
15
15
  import { replayRouterPayload } from "./lib/group-routing/replay.mjs";
16
- import { debounceBurst, routingBurst } from "./lib/group-routing/burst.mjs";
16
+ import { routingBurst } from "./lib/group-routing/burst.mjs";
17
17
  import { defineExtension } from "eve/extension";
18
18
  const extension = defineExtension({ config: configSchema });
19
19
  const segmentstream = Object.assign(extension, {
20
20
  model: (choice) => model(choice ?? (() => extension.config.models.chat)),
21
21
  manifestOf
22
22
  });
23
- export { DEFAULT_ADMISSION_LIMITS, DEFAULT_DECISION_MODEL, DEFAULT_GROUP_POLICY, DEFAULT_STT_MODEL, SegmentStreamAgentConfigError, archivedRoutingMessages, chooseModel, conversationKey, debounceBurst, segmentstream as default, segmentstream, effectiveGroupPolicy, addressingReason as groupAddressingReason, agentMessage as groupAgentMessage, conversationAddressing as groupConversationAddressing, decisionRouter as groupDecisionRouter, groupPolicy, manifestOf, model, replayRouterPayload, routeMessage as routeGroupMessage, routingBurst };
23
+ export { DEFAULT_ADMISSION_LIMITS, DEFAULT_DECISION_MODEL, DEFAULT_GROUP_POLICY, DEFAULT_STT_MODEL, SegmentStreamAgentConfigError, archivedRoutingMessages, chooseModel, conversationKey, segmentstream as default, segmentstream, effectiveGroupPolicy, addressingReason as groupAddressingReason, agentMessage as groupAgentMessage, conversationAddressing as groupConversationAddressing, decisionRouter as groupDecisionRouter, groupPolicy, manifestOf, model, replayRouterPayload, routeMessage as routeGroupMessage, routingBurst };
@@ -4,5 +4,5 @@ import { createRequire as __eveCreateRequire } from "node:module";
4
4
  const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
- import { t as agentBuild } from "../../_chunks/build-metadata-CMmCRD8l.mjs";
7
+ import { t as agentBuild } from "../../_chunks/build-metadata-Bqh43Sku.mjs";
8
8
  export { agentBuild };
@@ -9,20 +9,3 @@ export declare function routingBurst(signals: GroupMessageSignals, recent: Reado
9
9
  startedAt: string;
10
10
  latestAt: string;
11
11
  };
12
- /** Wait only inside the foreground webhook; every read checks durable, already captured messages. */
13
- export declare function debounceBurst(input: {
14
- signals: GroupMessageSignals;
15
- policy: GroupPolicy;
16
- read: () => Promise<ReadonlyArray<RecentMessage>>;
17
- sleep?: (ms: number) => Promise<void>;
18
- now?: () => number;
19
- }): Promise<{
20
- recent: readonly RecentMessage[];
21
- burst: {
22
- id: string;
23
- superseded: boolean;
24
- messages: RecentMessage[];
25
- startedAt: string;
26
- latestAt: string;
27
- };
28
- }>;
@@ -45,20 +45,4 @@ function routingBurst(signals, recent, policy) {
45
45
  latestAt: rows[end].at
46
46
  };
47
47
  }
48
- async function debounceBurst(input) {
49
- const now = input.now ?? Date.now;
50
- const sleep = input.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
51
- let recent = await input.read();
52
- let burst = routingBurst(input.signals, recent, input.policy);
53
- const deadline = Math.min(Date.parse(burst.latestAt) + input.policy.smart.engagement.burst.quietSeconds * 1e3, Date.parse(burst.startedAt) + input.policy.smart.engagement.burst.maxSeconds * 1e3);
54
- if (!burst.superseded && deadline > now()) {
55
- await sleep(deadline - now());
56
- recent = await input.read();
57
- burst = routingBurst(input.signals, recent, input.policy);
58
- }
59
- return {
60
- recent,
61
- burst
62
- };
63
- }
64
- export { debounceBurst, routingBurst };
48
+ export { routingBurst };
@@ -30,8 +30,6 @@ export interface SmartPolicy {
30
30
  readonly policy: "legacy" | "engaged" | "conversation";
31
31
  readonly agentAliases: ReadonlyArray<string>;
32
32
  readonly burst: {
33
- readonly quietSeconds: number;
34
- readonly maxSeconds: number;
35
33
  readonly gapSeconds: number;
36
34
  };
37
35
  readonly followUpMinutes: number;
@@ -59,13 +57,15 @@ export interface GroupPolicy {
59
57
  }
60
58
  export declare function smartPolicy(input?: SmartPolicyInput, base?: SmartPolicy): SmartPolicy;
61
59
  /**
62
- * Settings of the former conversation state machine (timers and per-state thresholds). They are
63
- * still accepted so that existing configurations deploy, and are ignored: the classifier reads the
64
- * chat's recent messages instead.
60
+ * Settings of former SDK mechanisms: the conversation state machine's timers and per-state
61
+ * thresholds, and the webhook's wait for the rest of a burst. They are still accepted so that
62
+ * existing configurations deploy, and are ignored: the classifier reads the chat's recent
63
+ * messages, and a member's next message joins their active turn through eve's steering.
65
64
  */
66
65
  export declare const IGNORED_GROUP_SETTINGS: {
67
66
  readonly engagement: readonly ["idleMinutes", "humanExchangeMinutes", "humanQuestionMinutes", "inferredMaxMinutes"];
68
67
  readonly thresholds: readonly ["continuation", "inferredContinuation", "contested", "ambient", "humanExchange"];
68
+ readonly burst: readonly ["quietSeconds", "maxSeconds"];
69
69
  };
70
70
  /** Whether a settings path names an ignored setting, in the agent, a chat or the shadow policy. */
71
71
  export declare function ignoredGroupSetting(path: ReadonlyArray<string>): boolean;
@@ -61,11 +61,9 @@ function smartPolicy(input = {}, base) {
61
61
  } : {},
62
62
  ...withoutIgnored(input.engagement, IGNORED_GROUP_SETTINGS.engagement),
63
63
  burst: {
64
- quietSeconds: 6,
65
- maxSeconds: 15,
66
64
  gapSeconds: 30,
67
65
  ...base?.engagement.burst,
68
- ...input.engagement?.burst
66
+ ...withoutIgnored(input.engagement?.burst, IGNORED_GROUP_SETTINGS.burst)
69
67
  }
70
68
  },
71
69
  thresholds: {
@@ -77,7 +75,6 @@ function smartPolicy(input = {}, base) {
77
75
  },
78
76
  timeoutSeconds: input.timeoutSeconds ?? base?.timeoutSeconds ?? 15
79
77
  };
80
- if (policy.engagement.burst.quietSeconds > policy.engagement.burst.maxSeconds) throw new RangeError("Burst quietSeconds must not exceed maxSeconds after inheritance.");
81
78
  if ((conversation ? policy.instructions.length + JSON.stringify(ADDRESSEE_CRITERIA).length + 260 : policy.instructions.length + policy.criteria.observe.length + policy.criteria.reply.length) > 8e3) throw new RangeError("Group instructions and criteria must fit 8000 characters after inheritance.");
82
79
  return policy;
83
80
  }
@@ -94,7 +91,8 @@ const IGNORED_GROUP_SETTINGS = {
94
91
  "contested",
95
92
  "ambient",
96
93
  "humanExchange"
97
- ]
94
+ ],
95
+ burst: ["quietSeconds", "maxSeconds"]
98
96
  };
99
97
  function withoutIgnored(value, ignored) {
100
98
  return Object.fromEntries(Object.entries(value ?? {}).filter(([key]) => !ignored.includes(key)));
@@ -23,7 +23,7 @@ import { archivedRoutingMessages } from "../group-routing/routing-context.mjs";
23
23
  import { dispatchStructuredMessage, structuredMessage } from "./structured.mjs";
24
24
  import { scopedAddress } from "./sessions.mjs";
25
25
  import { handleWorkspaceCallback, isWorkspaceCallback } from "./callbacks.mjs";
26
- import { t as agentBuild } from "../../../_chunks/build-metadata-CMmCRD8l.mjs";
26
+ import { t as agentBuild } from "../../../_chunks/build-metadata-Bqh43Sku.mjs";
27
27
  import { attachmentLifecycle } from "./lifecycle.mjs";
28
28
  import { gatewayImageDescriber } from "./images.mjs";
29
29
  import { gatewayAttachmentDescriber } from "./attachment-description.mjs";
@@ -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-CMmCRD8l.mjs";
8
+ import { t as agentBuild } from "../../../_chunks/build-metadata-Bqh43Sku.mjs";
9
9
  import { GET } from "eve/channels";
10
10
  const HEALTH_ROUTE = "/eve/v1/telegram/health";
11
11
  function healthRoute(input) {
@@ -29,7 +29,7 @@ export declare class InboundRequest {
29
29
  readonly agentDescription: () => Promise<string>;
30
30
  readonly agentName: () => Promise<string>;
31
31
  readonly traceId: `${string}-${string}-${string}-${string}-${string}`;
32
- /** Conversation policy coalesces only the active caller; Eve queues other callers. */
32
+ /** Conversation policy steers the active caller's turn; Eve queues other callers. */
33
33
  turnPolicy: "queue" | "steer";
34
34
  edited: boolean;
35
35
  triggeringMessageId: string | undefined;
@@ -9,10 +9,9 @@ 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 } from "../group-routing/addressing.mjs";
13
12
  import { questionInvitation } from "../group-routing/engagement.mjs";
14
13
  import { routeMessage } from "../group-routing/router.mjs";
15
- import { debounceBurst, routingBurst } from "../group-routing/burst.mjs";
14
+ import { routingBurst } from "../group-routing/burst.mjs";
16
15
  import { WorkspaceActionAlreadyDispatched } from "./workspace-action.mjs";
17
16
  import { structuredMessage } from "./structured.mjs";
18
17
  import { formatAddress, messageAddress, scopedAddress } from "./sessions.mjs";
@@ -495,11 +494,6 @@ async function handleInboundMessage(input) {
495
494
  model: dependencies.decisionModel
496
495
  }), conversationKey(policySignals));
497
496
  const conversationPolicy = group && groups.smart.engagement.policy === "conversation";
498
- const agentAliases = [...groups.smart.engagement.agentAliases, ...[
499
- bot?.username,
500
- bot?.firstName,
501
- bot?.displayName
502
- ].filter((n) => !!n)];
503
497
  let conversationRecent = conversationPolicy ? await recentConversation() : [];
504
498
  if (conversationPolicy && input.request && input.request.lifecycleKey === void 0 && input.request.structuredContext === void 0) {
505
499
  address = topicAddress;
@@ -626,7 +620,6 @@ async function handleInboundMessage(input) {
626
620
  };
627
621
  return question;
628
622
  };
629
- let burstId;
630
623
  if (conversationPolicy && input.request?.lifecycleKey === void 0 && input.request?.structuredContext === void 0) {
631
624
  if (voice !== void 0) await transcribeVoice(true);
632
625
  const routingSignals = {
@@ -636,26 +629,7 @@ async function handleInboundMessage(input) {
636
629
  text
637
630
  }
638
631
  };
639
- const evidence = conversationAddressing(routingSignals, conversationRecent, groups, agentAliases);
640
- if (!evidence.agent_named && !evidence.reply_to_agent && evidence.human_targets.length === 0) {
641
- await recordContent({
642
- text,
643
- decoded
644
- });
645
- const refreshed = await debounceBurst({
646
- signals: routingSignals,
647
- policy: groups,
648
- read: freshConversation
649
- });
650
- conversationRecent = refreshed.recent;
651
- question = void 0;
652
- if (refreshed.burst.superseded) {
653
- recordGateDecision("burst_superseded");
654
- return null;
655
- }
656
- }
657
632
  const burst = routingBurst(routingSignals, conversationRecent, groups);
658
- burstId = burst.id;
659
633
  const q = await conversation();
660
634
  question = {
661
635
  ...q,
@@ -1054,32 +1028,6 @@ async function handleInboundMessage(input) {
1054
1028
  chatGuidance: guidance
1055
1029
  }
1056
1030
  };
1057
- if (conversationPolicy && input.request?.lifecycleKey === void 0 && input.request?.structuredContext === void 0) principal = {
1058
- ...principal,
1059
- attributes: {
1060
- ...principal.attributes,
1061
- groupRouting: JSON.stringify({
1062
- policy: {
1063
- ...groups,
1064
- chats: {}
1065
- },
1066
- signals: {
1067
- ...policySignals,
1068
- message: {
1069
- ...policySignals.message,
1070
- text
1071
- }
1072
- },
1073
- agent: question?.agent.slice(0, 600) ?? "",
1074
- agentIdentity: question?.agentIdentity,
1075
- chat: question?.chat,
1076
- burst_id: burstId ?? routingBurst(policySignals, conversationRecent, groups).id,
1077
- message: question?.message,
1078
- pendingQuestions: question?.pendingQuestions,
1079
- pendingQuestionAt: asked?.meta.at
1080
- })
1081
- }
1082
- };
1083
1031
  return {
1084
1032
  auth: answering && pending !== void 0 ? {
1085
1033
  ...principal,
@@ -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-CMmCRD8l.mjs";
13
+ import { t as agentBuild } from "../../../_chunks/build-metadata-Bqh43Sku.mjs";
14
14
  var AttachmentLifecycleNotPermitted = class extends Error {
15
15
  code = "attachment_lifecycle_not_permitted";
16
16
  };
@@ -9,7 +9,6 @@ import { i as memberIdOf } from "../../../_chunks/principal-D99Lleae.mjs";
9
9
  import { confirmationLocale } from "./confirmation-text.mjs";
10
10
  import { c as customerCopy, d as failureText, o as currentLocale, p as reportTelegramError, s as currentThreadId } from "../../../_chunks/inbound-request-D1r0FkDI.mjs";
11
11
  import { recordSessionFailure } from "./conversations.mjs";
12
- import { isSilentRoutingCancellation } from "../group-routing/cancellation.mjs";
13
12
  import { modelServiceFailure } from "../model-service-failure.mjs";
14
13
  function telegramFailureEvents(input) {
15
14
  return {
@@ -49,15 +48,6 @@ function telegramFailureEvents(input) {
49
48
  const authorizationTurn = state.authorizationCancellationTurnId;
50
49
  delete state.authorizationCancellationTurnId;
51
50
  if (typeof event.turnId === "string" && authorizationTurn === event.turnId) return;
52
- try {
53
- if (await isSilentRoutingCancellation(input.archive, context, event.turnId)) return;
54
- } catch (error) {
55
- reportTelegramError("routing_cancellation_lookup_failed", error, {
56
- sessionId: context.session.id,
57
- turnId: event.turnId
58
- });
59
- return;
60
- }
61
51
  const errorId = reportTelegramError("turn_cancelled", event, {
62
52
  chatId: channel.state.chatId,
63
53
  sessionId: context.session.id,
@@ -73,15 +63,6 @@ function telegramFailureEvents(input) {
73
63
  sessionId: context.session.id,
74
64
  turnId: event.turnId
75
65
  });
76
- try {
77
- if (await isSilentRoutingCancellation(input.archive, context, event.turnId)) return;
78
- } catch (error) {
79
- reportTelegramError("routing_failure_lookup_failed", error, {
80
- sessionId: context.session.id,
81
- turnId: event.turnId
82
- });
83
- return;
84
- }
85
66
  await channel.telegram.post(failureText(errorId, modelServiceFailure(event.error) ?? "failed", confirmationLocale(context.session.auth.current, currentLocale())));
86
67
  },
87
68
  async "session.ended"(fact, context) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream/agent",
3
- "version": "0.28.0",
3
+ "version": "0.29.1",
4
4
  "description": "Mount a SegmentStream workspace's agent on eve: the ontology as tools, acting as the member who writes, and Telegram through the workspace's connection.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "SegmentStream",
@@ -74,7 +74,7 @@
74
74
  },
75
75
  "dependencies": {
76
76
  "@chat-adapter/telegram": "4.41.0",
77
- "@segmentstream-cloud/ontology": "0.80.1",
77
+ "@segmentstream-cloud/ontology": "0.80.2",
78
78
  "@vercel/oidc": "^3.8.4",
79
79
  "ai": "^7.0.137",
80
80
  "csv-parse": "^7.0.3",
@@ -1,2 +0,0 @@
1
- declare const _default: import("eve/hooks").HookDefinition<"turn.started">;
2
- export default _default;
@@ -1,10 +0,0 @@
1
- import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
- import { dirname as __eveDirname } from "node:path";
3
- import { createRequire as __eveCreateRequire } from "node:module";
4
- const __filename = __eveFileURLToPath(import.meta.url);
5
- __eveDirname(__filename);
6
- __eveCreateRequire(import.meta.url);
7
- import { conversationDequeueGuard } from "../lib/group-routing/dequeue.mjs";
8
- import { defineHook } from "eve/hooks";
9
- var group_routing_default = defineHook({ events: { "turn.started": conversationDequeueGuard() } });
10
- export { group_routing_default as default };
@@ -1,4 +0,0 @@
1
- import type { SessionContext } from "eve/context";
2
- import type { Conversations } from "../conversations.js";
3
- /** The durable routing verdict owns silence, including after a channel callback is replayed. */
4
- export declare function isSilentRoutingCancellation(archive: Conversations, context: SessionContext, turnId: string): Promise<boolean>;
@@ -1,22 +0,0 @@
1
- import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
- import { dirname as __eveDirname } from "node:path";
3
- import { createRequire as __eveCreateRequire } from "node:module";
4
- const __filename = __eveFileURLToPath(import.meta.url);
5
- __eveDirname(__filename);
6
- __eveCreateRequire(import.meta.url);
7
- async function isSilentRoutingCancellation(archive, context, turnId) {
8
- const principal = context.session.auth.current;
9
- const attributes = principal?.attributes;
10
- if (principal?.authenticator !== "segmentstream-telegram" || typeof attributes?.groupRouting !== "string" || attributes.chatType !== "group" && attributes.chatType !== "supergroup" || typeof attributes.chatId !== "string") return false;
11
- const { events } = await archive.context({
12
- provider: "telegram",
13
- chatId: attributes.chatId,
14
- ...typeof attributes.messageThreadId === "string" ? { threadId: attributes.messageThreadId } : {},
15
- limit: 200,
16
- includeDecisions: true,
17
- ...typeof attributes.replyToMessageId === "string" ? { messageId: attributes.replyToMessageId } : {}
18
- });
19
- const scoped = events.filter((event) => event.provider === "telegram" && event.chatId === attributes.chatId && event.threadId === (attributes.messageThreadId ?? null) && event.sessionId === context.session.id && event.turnId === turnId && event.kind === "decision");
20
- return scoped.some((event) => event.decision === "silent" && (event.reason === "burst_already_answered" || event.reason === "dequeue_observe")) || !scoped.some((event) => event.decision === "reply" && (event.reason === "burst_turn_started" || event.reason === "dequeue_reply"));
21
- }
22
- export { isSilentRoutingCancellation };
@@ -1,5 +0,0 @@
1
- import type { HookContext, HookEvent } from "eve/hooks";
2
- import { type Conversations } from "../conversations.js";
3
- import { type ConversationDecideFunction } from "./conversation-router.js";
4
- /** Recheck queued callers at the durable turn boundary, before any model or business action. */
5
- export declare function conversationDequeueGuard(archive?: Conversations, classifier?: ConversationDecideFunction): (event: HookEvent<"turn.started">, ctx: HookContext) => Promise<void>;
@@ -1,145 +0,0 @@
1
- import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
- import { dirname as __eveDirname } from "node:path";
3
- import { createRequire as __eveCreateRequire } from "node:module";
4
- const __filename = __eveFileURLToPath(import.meta.url);
5
- __eveDirname(__filename);
6
- __eveCreateRequire(import.meta.url);
7
- import { conversationEvent, conversations } from "../conversations.mjs";
8
- import { addressingReason } from "./signals.mjs";
9
- import { recordedMessageSignals } from "./addressing.mjs";
10
- import { conversationDecisionRouter } from "./conversation-router.mjs";
11
- import { routeMessage } from "./router.mjs";
12
- import { archivedRoutingMessages } from "./routing-context.mjs";
13
- import { routingBurst } from "./burst.mjs";
14
- import { conversationCoordinates } from "../conversation-runtime.mjs";
15
- import { createHash } from "node:crypto";
16
- import { decide } from "eve/ai";
17
- function conversationDequeueGuard(archive = conversations(), classifier = decide) {
18
- return async (event, ctx) => {
19
- const auth = ctx.session.auth.current;
20
- const encoded = auth?.attributes.groupRouting;
21
- const coordinates = conversationCoordinates(ctx);
22
- if (auth?.authenticator !== "segmentstream-telegram" || typeof encoded !== "string" || coordinates?.chatType !== "group") return;
23
- try {
24
- const input = JSON.parse(encoded);
25
- if (input.policy.smart.engagement.policy !== "conversation") return;
26
- if (input.signals.sender.externalId !== auth.subject || input.signals.conversation.externalId !== coordinates.chatId) throw new Error("Routing provenance does not match the turn caller.");
27
- const events = (await archive.context({
28
- provider: coordinates.provider,
29
- chatId: coordinates.chatId,
30
- ...coordinates.threadId === null ? {} : { threadId: coordinates.threadId },
31
- limit: 200,
32
- includeDecisions: true
33
- })).events;
34
- const fence = `group-routing:burst:${createHash("sha256").update(input.burst_id).digest("hex")}`;
35
- const existing = events.find((entry) => entry.eventId === fence);
36
- if (existing !== void 0) {
37
- if (existing.turnId !== ctx.session.turn.id) {
38
- await archive.append(conversationEvent({
39
- ...coordinates,
40
- eventId: `${fence}:duplicate:${ctx.session.turn.id}`,
41
- sessionId: ctx.session.id,
42
- turnId: ctx.session.turn.id,
43
- messageId: input.signals.message.id,
44
- kind: "decision",
45
- decision: "silent",
46
- reason: "burst_already_answered",
47
- argsSummary: JSON.stringify({ burst_id: input.burst_id })
48
- }));
49
- ctx.cancel();
50
- }
51
- return;
52
- }
53
- const recent = archivedRoutingMessages(events, 100);
54
- const decisions = [];
55
- const burst = routingBurst(input.signals, recent, input.policy);
56
- const latest = burst.messages.at(-1);
57
- const signals = recordedMessageSignals(latest, input.signals);
58
- const claimedAddressing = addressingReason(input.signals);
59
- const explicit = claimedAddressing === "mention" || claimedAddressing === "command";
60
- const route = explicit ? "reply" : await routeMessage({
61
- groups: input.policy,
62
- signals,
63
- policy: input.policy.mode,
64
- chatId: coordinates.chatId,
65
- memberId: input.signals.sender.externalId,
66
- addressed: void 0,
67
- pendingQuestionAt: input.pendingQuestionAt,
68
- question: async () => ({
69
- groups: input.policy,
70
- signals,
71
- agent: input.agent,
72
- processingAt: ctx.now.toISOString(),
73
- agentIdentity: input.agentIdentity,
74
- chat: input.chat,
75
- recent,
76
- burstId: input.burst_id,
77
- messagesToClassify: burst.messages,
78
- pendingQuestions: input.pendingQuestions,
79
- message: {
80
- ...input.message,
81
- ...latest
82
- }
83
- }),
84
- decide: conversationDecisionRouter(classifier),
85
- record: (d) => decisions.push(d)
86
- });
87
- const base = {
88
- ...coordinates,
89
- sessionId: ctx.session.id,
90
- turnId: ctx.session.turn.id,
91
- at: ctx.now.toISOString(),
92
- messageId: input.signals.message.id
93
- };
94
- if (route === "reply") {
95
- if (await archive.append(conversationEvent({
96
- ...base,
97
- eventId: fence,
98
- kind: "decision",
99
- decision: "reply",
100
- reason: "burst_turn_started",
101
- argsSummary: JSON.stringify({ burst_id: input.burst_id })
102
- })) !== true) {
103
- if (!(await archive.context({
104
- provider: coordinates.provider,
105
- chatId: coordinates.chatId,
106
- ...coordinates.threadId === null ? {} : { threadId: coordinates.threadId },
107
- limit: 200,
108
- includeDecisions: true
109
- })).events.find((e) => e.eventId === fence && e.turnId === ctx.session.turn.id)) {
110
- await archive.append(conversationEvent({
111
- ...base,
112
- eventId: `${fence}:duplicate:${ctx.session.turn.id}`,
113
- kind: "decision",
114
- decision: "silent",
115
- reason: "burst_already_answered",
116
- argsSummary: JSON.stringify({ burst_id: input.burst_id })
117
- }));
118
- ctx.cancel();
119
- return;
120
- }
121
- }
122
- }
123
- await archive.append(conversationEvent({
124
- ...base,
125
- eventId: `group-routing:dequeue:${ctx.session.turn.id}`,
126
- kind: "decision",
127
- decision: route === "reply" ? "reply" : "silent",
128
- reason: explicit ? "dequeue_explicit_kept" : `dequeue_${route}`,
129
- argsSummary: JSON.stringify({
130
- router: decisions.at(-1)?.router,
131
- burst_id: input.burst_id,
132
- claimed_addressing: explicit ? claimedAddressing : void 0
133
- })
134
- }));
135
- if (route === "observe") ctx.cancel();
136
- } catch (error) {
137
- console.error("[segmentstream] conversation dequeue routing failed", {
138
- error,
139
- sessionId: ctx.session.id
140
- });
141
- ctx.cancel();
142
- }
143
- };
144
- }
145
- export { conversationDequeueGuard };