@holon-run/agentinbox 1.3.3 → 1.4.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,6 +4,7 @@ exports.AdapterRegistry = void 0;
4
4
  const paths_1 = require("./paths");
5
5
  const feishu_1 = require("./sources/feishu");
6
6
  const github_1 = require("./sources/github");
7
+ const telegram_1 = require("./sources/telegram");
7
8
  const remote_1 = require("./sources/remote");
8
9
  const remote_modules_1 = require("./sources/remote_modules");
9
10
  const source_resolution_1 = require("./source_resolution");
@@ -40,6 +41,7 @@ class AdapterRegistry {
40
41
  defaultDelivery = new NoopDeliveryAdapter();
41
42
  feishuDelivery = new feishu_1.FeishuDeliveryAdapter();
42
43
  githubDelivery = new github_1.GithubDeliveryAdapter();
44
+ telegramDelivery;
43
45
  homeDir;
44
46
  remoteModuleRegistry;
45
47
  constructor(store, appendSourceEvent, options) {
@@ -47,6 +49,7 @@ class AdapterRegistry {
47
49
  this.remoteModuleRegistry = options?.remoteModuleRegistry ?? new remote_modules_1.RemoteSourceModuleRegistry({
48
50
  githubCallClient: options?.githubCallClient,
49
51
  });
52
+ this.telegramDelivery = new telegram_1.TelegramDeliveryAdapter(options?.telegramClient);
50
53
  this.remoteSource = new remote_1.RemoteSourceRuntime(store, appendSourceEvent, {
51
54
  homeDir: this.homeDir,
52
55
  client: options?.remoteSourceClient,
@@ -69,6 +72,9 @@ class AdapterRegistry {
69
72
  if (type === "feishu_bot") {
70
73
  return this.remoteSource;
71
74
  }
75
+ if (type === "telegram_bot") {
76
+ return this.remoteSource;
77
+ }
72
78
  return this.localEventSource;
73
79
  }
74
80
  deliveryAdapterFor(provider) {
@@ -78,6 +84,9 @@ class AdapterRegistry {
78
84
  if (provider === "github") {
79
85
  return this.githubDelivery;
80
86
  }
87
+ if (provider === "telegram") {
88
+ return this.telegramDelivery;
89
+ }
81
90
  return this.defaultDelivery;
82
91
  }
83
92
  async start() {
@@ -238,6 +247,9 @@ class AdapterRegistry {
238
247
  if (handle.provider === "feishu") {
239
248
  return this.remoteModuleRegistry.resolve(syntheticBuiltinSource("feishu_bot"), this.homeDir);
240
249
  }
250
+ if (handle.provider === "telegram") {
251
+ return this.remoteModuleRegistry.resolve(syntheticBuiltinSource("telegram_bot"), this.homeDir);
252
+ }
241
253
  return null;
242
254
  }
243
255
  }
@@ -248,6 +260,7 @@ function isExplicitFollowPreviewRef(value) {
248
260
  value === "github_repo" ||
249
261
  value === "github_repo_ci" ||
250
262
  value === "feishu_bot" ||
263
+ value === "telegram_bot" ||
251
264
  value.startsWith("remote:"));
252
265
  }
253
266
  function hasRemoteSourceModulePath(config) {
@@ -1697,6 +1697,15 @@ class AgentInboxService {
1697
1697
  }
1698
1698
  async status() {
1699
1699
  const sources = await this.listSourceViews();
1700
+ const agents = this.store.listAgents();
1701
+ const subscriptions = this.store.listSubscriptions();
1702
+ const inboxes = this.store.listInboxes();
1703
+ const activationTargets = this.store.listActivationTargets();
1704
+ const activationDispatchStates = this.store.listActivationDispatchStates();
1705
+ const streams = this.store.listStreams();
1706
+ const consumers = this.store.listConsumers();
1707
+ const recentActivations = this.store.listActivations().slice(0, 10);
1708
+ const recentDeliveries = this.store.listDeliveries().slice(0, 10);
1700
1709
  return {
1701
1710
  retention: {
1702
1711
  ackedInboxItemsMs: this.ackedRetentionMs,
@@ -1704,17 +1713,30 @@ class AgentInboxService {
1704
1713
  lastAckedInboxGcAt: this.lastAckedInboxGcAt > 0 ? new Date(this.lastAckedInboxGcAt).toISOString() : null,
1705
1714
  },
1706
1715
  counts: this.store.getCounts(),
1707
- agents: this.store.listAgents(),
1716
+ diagnostics: {
1717
+ database: this.store.getDatabaseHealth(),
1718
+ daemon: { status: "running" },
1719
+ sources: summarizeByStatus(sources, ["active", "paused", "error"]),
1720
+ agents: summarizeByStatus(agents, ["active", "offline"]),
1721
+ activationTargets: summarizeByStatus(activationTargets, ["active", "offline"]),
1722
+ activationDispatch: {
1723
+ ...summarizeByStatus(activationDispatchStates, ["dirty", "notified"]),
1724
+ pendingNewItems: activationDispatchStates.reduce((total, state) => total + state.pendingNewItemCount, 0),
1725
+ deferredAttempts: activationDispatchStates.reduce((total, state) => total + state.deferAttempts, 0),
1726
+ },
1727
+ deliveries: summarizeByStatus(recentDeliveries, ["accepted", "sent", "failed"]),
1728
+ },
1729
+ agents,
1708
1730
  sources,
1709
- subscriptions: this.store.listSubscriptions(),
1710
- inboxes: this.store.listInboxes(),
1711
- activationTargets: this.store.listActivationTargets(),
1712
- activationDispatchStates: this.store.listActivationDispatchStates(),
1713
- streams: this.store.listStreams(),
1714
- consumers: this.store.listConsumers(),
1731
+ subscriptions,
1732
+ inboxes,
1733
+ activationTargets,
1734
+ activationDispatchStates,
1735
+ streams,
1736
+ consumers,
1715
1737
  adapters: this.adapters.status(),
1716
- recentActivations: this.store.listActivations().slice(0, 10),
1717
- recentDeliveries: this.store.listDeliveries().slice(0, 10),
1738
+ recentActivations,
1739
+ recentDeliveries,
1718
1740
  lifecycle: {
1719
1741
  offlineAgentTtlMs: DEFAULT_OFFLINE_AGENT_TTL_MS,
1720
1742
  gcIntervalMs: DEFAULT_GC_INTERVAL_MS,
@@ -1993,6 +2015,7 @@ class AgentInboxService {
1993
2015
  buffer = {
1994
2016
  agentId: target.agentId,
1995
2017
  targetId: target.targetId,
2018
+ targetKind: target.kind,
1996
2019
  pending: [],
1997
2020
  timer: null,
1998
2021
  inFlight: false,
@@ -2081,21 +2104,24 @@ class AgentInboxService {
2081
2104
  return;
2082
2105
  }
2083
2106
  const state = this.store.getActivationDispatchState(buffer.agentId, buffer.targetId);
2084
- const shouldAttemptDispatch = !state || (state.status === "dirty" && state.leaseExpiresAt == null);
2107
+ const shouldAttemptDispatch = !state
2108
+ || (state.status === "dirty" && state.leaseExpiresAt == null)
2109
+ || (state.status === "notified" && buffer.targetKind === "webhook");
2085
2110
  if (shouldAttemptDispatch) {
2111
+ const pendingState = state?.status === "dirty" ? state : null;
2086
2112
  const inbox = this.ensureInboxForAgent(buffer.agentId);
2087
2113
  const unackedItems = this.store.listInboxEntries(inbox.inboxId, { includeAcked: false });
2088
2114
  const dispatched = await this.dispatchActivationTarget({
2089
2115
  agentId: buffer.agentId,
2090
2116
  targetId: buffer.targetId,
2091
- newItemCount: state ? state.pendingNewItemCount + entries.length : entries.length,
2117
+ newItemCount: pendingState ? pendingState.pendingNewItemCount + entries.length : entries.length,
2092
2118
  totalUnackedCount: unackedItems.length,
2093
- summary: state ? latestSummary(entries) ?? state.pendingSummary : latestSummary(entries),
2094
- subscriptionIds: state
2095
- ? uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)])
2119
+ summary: pendingState ? latestSummary(entries) ?? pendingState.pendingSummary : latestSummary(entries),
2120
+ subscriptionIds: pendingState
2121
+ ? uniqueSortedNullable([...pendingState.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)])
2096
2122
  : uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
2097
- sourceIds: state
2098
- ? uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)])
2123
+ sourceIds: pendingState
2124
+ ? uniqueSorted([...pendingState.pendingSourceIds, ...entries.map((entry) => entry.sourceId)])
2099
2125
  : uniqueSorted(entries.map((entry) => entry.sourceId)),
2100
2126
  entries: unackedItems,
2101
2127
  });
@@ -2104,22 +2130,22 @@ class AgentInboxService {
2104
2130
  this.store.deleteActivationDispatchState(buffer.agentId, buffer.targetId);
2105
2131
  }
2106
2132
  if (dispatched === "retryable_failure") {
2107
- if (state && state.status === "dirty" && state.leaseExpiresAt == null) {
2133
+ if (pendingState && pendingState.leaseExpiresAt == null) {
2108
2134
  this.store.upsertActivationDispatchState({
2109
2135
  agentId: buffer.agentId,
2110
2136
  targetId: buffer.targetId,
2111
2137
  status: "dirty",
2112
2138
  leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
2113
- lastNotifiedFingerprint: state.lastNotifiedFingerprint,
2139
+ lastNotifiedFingerprint: pendingState.lastNotifiedFingerprint,
2114
2140
  deferReason: null,
2115
2141
  deferAttempts: 0,
2116
2142
  firstDeferredAt: null,
2117
2143
  lastDeferredAt: null,
2118
2144
  pendingFingerprint: null,
2119
- pendingNewItemCount: state.pendingNewItemCount + entries.length,
2120
- pendingSummary: latestSummary(entries) ?? state.pendingSummary,
2121
- pendingSubscriptionIds: uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)]),
2122
- pendingSourceIds: uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)]),
2145
+ pendingNewItemCount: pendingState.pendingNewItemCount + entries.length,
2146
+ pendingSummary: latestSummary(entries) ?? pendingState.pendingSummary,
2147
+ pendingSubscriptionIds: uniqueSortedNullable([...pendingState.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)]),
2148
+ pendingSourceIds: uniqueSorted([...pendingState.pendingSourceIds, ...entries.map((entry) => entry.sourceId)]),
2123
2149
  updatedAt: (0, util_1.nowIso)(),
2124
2150
  });
2125
2151
  }
@@ -3104,7 +3130,12 @@ function mergeFollowPreviewConfig(config, templateArgs) {
3104
3130
  };
3105
3131
  }
3106
3132
  function sourceTypeForPreviewRef(sourceRef) {
3107
- if (sourceRef === "local_event" || sourceRef === "remote_source" || sourceRef === "github_repo" || sourceRef === "github_repo_ci" || sourceRef === "feishu_bot") {
3133
+ if (sourceRef === "local_event" ||
3134
+ sourceRef === "remote_source" ||
3135
+ sourceRef === "github_repo" ||
3136
+ sourceRef === "github_repo_ci" ||
3137
+ sourceRef === "feishu_bot" ||
3138
+ sourceRef === "telegram_bot") {
3108
3139
  return sourceRef;
3109
3140
  }
3110
3141
  if (sourceRef.startsWith("remote:")) {
@@ -3403,6 +3434,9 @@ function providerForSourceType(sourceType) {
3403
3434
  if (sourceType === "feishu_bot") {
3404
3435
  return "feishu";
3405
3436
  }
3437
+ if (sourceType === "telegram_bot") {
3438
+ return "telegram";
3439
+ }
3406
3440
  return null;
3407
3441
  }
3408
3442
  class ActivationDispatcher {
@@ -3972,6 +4006,8 @@ function listHostStreamKinds(hostType) {
3972
4006
  return ["repo_events", "ci_runs"];
3973
4007
  case "feishu":
3974
4008
  return ["message_events"];
4009
+ case "telegram":
4010
+ return ["message_updates"];
3975
4011
  case "local_event":
3976
4012
  return ["events"];
3977
4013
  case "remote_source":
@@ -4015,6 +4051,16 @@ function deliveryActionsHelp(handle) {
4015
4051
  function isKnownDeliverySurface(handle) {
4016
4052
  return KNOWN_DELIVERY_SURFACES[handle.provider]?.includes(handle.surface) === true;
4017
4053
  }
4054
+ function summarizeByStatus(records, expectedStatuses) {
4055
+ const summary = { total: records.length };
4056
+ for (const status of expectedStatuses) {
4057
+ summary[status] = 0;
4058
+ }
4059
+ for (const record of records) {
4060
+ summary[record.status] = (summary[record.status] ?? 0) + 1;
4061
+ }
4062
+ return summary;
4063
+ }
4018
4064
  function getHostConfigFields(hostType) {
4019
4065
  switch (hostType) {
4020
4066
  case "github":
@@ -4025,6 +4071,12 @@ function getHostConfigFields(hostType) {
4025
4071
  return [
4026
4072
  { name: "uxcAuth", type: "string", description: "Optional shared Feishu/Lark UXC auth profile.", required: false },
4027
4073
  ];
4074
+ case "telegram":
4075
+ return [
4076
+ { name: "botToken", type: "string", description: "Telegram Bot API token.", required: false },
4077
+ { name: "tokenEnv", type: "string", description: "Environment variable containing the Telegram Bot API token.", required: false },
4078
+ { name: "botUsername", type: "string", description: "Optional bot username used for shared host identity.", required: false },
4079
+ ];
4028
4080
  case "local_event":
4029
4081
  return [];
4030
4082
  case "remote_source":
@@ -4041,6 +4093,9 @@ function sourceTypeForStreamRegistration(hostType, streamKind) {
4041
4093
  if (hostType === "feishu") {
4042
4094
  return "feishu_bot";
4043
4095
  }
4096
+ if (hostType === "telegram") {
4097
+ return "telegram_bot";
4098
+ }
4044
4099
  if (hostType === "local_event") {
4045
4100
  return "local_event";
4046
4101
  }
@@ -44,6 +44,22 @@ function resolveSourceRegistration(input) {
44
44
  sourceType: input.sourceType,
45
45
  };
46
46
  }
47
+ if (input.sourceType === "telegram_bot") {
48
+ return {
49
+ hostType: "telegram",
50
+ hostKey: `bot:${stringOrDefault(config.botUsername, input.configRef ?? input.sourceKey)}`,
51
+ hostConfig: {
52
+ ...(valueOrUndefined(config.botToken) ? { botToken: config.botToken } : {}),
53
+ ...(valueOrUndefined(config.tokenEnv) ? { tokenEnv: config.tokenEnv } : {}),
54
+ ...(valueOrUndefined(config.apiBaseUrl) ? { apiBaseUrl: config.apiBaseUrl } : {}),
55
+ ...(valueOrUndefined(config.botUsername) ? { botUsername: config.botUsername } : {}),
56
+ },
57
+ streamKind: "message_updates",
58
+ streamKey: input.sourceKey,
59
+ streamConfig: config,
60
+ sourceType: input.sourceType,
61
+ };
62
+ }
47
63
  if (input.sourceType === "local_event") {
48
64
  return {
49
65
  hostType: "local_event",
@@ -154,6 +154,41 @@ const SOURCE_SCHEMAS = {
154
154
  { name: "replyInThread", type: "boolean", required: false, description: "Reply in thread when sending outbound messages." },
155
155
  ],
156
156
  },
157
+ telegram_bot: {
158
+ sourceType: "telegram_bot",
159
+ metadataFields: [
160
+ { name: "updateId", type: "string", description: "Telegram update_id." },
161
+ { name: "chatId", type: "string", description: "Telegram chat ID." },
162
+ { name: "chatType", type: "string|null", description: "Telegram chat type such as private, group, or channel." },
163
+ { name: "messageId", type: "string", description: "Telegram message_id." },
164
+ { name: "messageType", type: "string", description: "Normalized Telegram message type such as text or photo." },
165
+ { name: "fromId", type: "string|null", description: "Telegram sender user ID when present." },
166
+ { name: "fromUsername", type: "string|null", description: "Telegram sender username when present." },
167
+ { name: "fromFirstName", type: "string|null", description: "Telegram sender first_name when present." },
168
+ { name: "content", type: "string|null", description: "Message text or caption when present." },
169
+ ],
170
+ payloadExamples: [
171
+ {
172
+ update_id: 123,
173
+ message: {
174
+ message_id: 7,
175
+ date: 1710000000,
176
+ text: "hello",
177
+ chat: { id: 456, type: "private" },
178
+ from: { id: 111, username: "operator", first_name: "Op" },
179
+ },
180
+ },
181
+ ],
182
+ eventVariantExamples: ["message.text", "edited_message.text", "channel_post.photo"],
183
+ configFields: [
184
+ { name: "botToken", type: "string", required: false, description: "Telegram bot token. Prefer tokenEnv for shared/local configuration." },
185
+ { name: "tokenEnv", type: "string", required: false, description: "Environment variable name containing the Telegram bot token." },
186
+ { name: "botUsername", type: "string", required: false, description: "Optional bot username used for source host identity." },
187
+ { name: "chatIds", type: "string[]", required: false, description: "Optional Telegram chat ID allowlist." },
188
+ { name: "allowedUpdates", type: "string[]", required: false, description: "Optional Telegram Bot API allowed_updates value." },
189
+ { name: "endpoint", type: "string", required: false, description: "Optional Telegram Bot API endpoint override." },
190
+ ],
191
+ },
157
192
  };
158
193
  function getSourceSchema(sourceType) {
159
194
  const schema = SOURCE_SCHEMAS[sourceType];
@@ -10,6 +10,7 @@ const REMOTE_SOURCE_TYPES = new Set([
10
10
  "github_repo",
11
11
  "github_repo_ci",
12
12
  "feishu_bot",
13
+ "telegram_bot",
13
14
  ]);
14
15
  const DEFAULT_SYNC_INTERVAL_MS = 2_000;
15
16
  const DEFAULT_BACKOFF_BASE_SECS = 2;
@@ -21,6 +22,7 @@ class RpcUxcRemoteSourceClient {
21
22
  this.client = client;
22
23
  }
23
24
  sourceEnsure(args) {
25
+ const transportHint = args.spec.transport_hint ?? null;
24
26
  return this.client.sourceEnsure({
25
27
  namespace: args.namespace,
26
28
  sourceKey: args.sourceKey,
@@ -29,7 +31,7 @@ class RpcUxcRemoteSourceClient {
29
31
  operation_id: args.spec.operation_id ?? null,
30
32
  resource_uri: args.spec.resource_uri ?? null,
31
33
  read_resource: args.spec.read_resource ?? false,
32
- transport_hint: args.spec.transport_hint ?? null,
34
+ transport_hint: transportHint,
33
35
  subprotocols: args.spec.subprotocols ?? [],
34
36
  initial_text_frames: args.spec.initial_text_frames ?? [],
35
37
  poll_config: args.spec.poll_config ?? null,
@@ -14,9 +14,10 @@ const node_url_1 = require("node:url");
14
14
  const github_1 = require("./github");
15
15
  const github_ci_1 = require("./github_ci");
16
16
  const feishu_1 = require("./feishu");
17
+ const telegram_1 = require("./telegram");
17
18
  const REMOTE_USER_MODULE_ROOT_DIR = "source-modules";
18
- const BUILTIN_MODULE_IDS = new Set(["builtin.github_repo", "builtin.github_repo_ci", "builtin.feishu_bot"]);
19
- const BUILTIN_REMOTE_SOURCE_TYPES = ["github_repo", "github_repo_ci", "feishu_bot"];
19
+ const BUILTIN_MODULE_IDS = new Set(["builtin.github_repo", "builtin.github_repo_ci", "builtin.feishu_bot", "builtin.telegram_bot"]);
20
+ const BUILTIN_REMOTE_SOURCE_TYPES = ["github_repo", "github_repo_ci", "feishu_bot", "telegram_bot"];
20
21
  class RemoteSourceModuleRegistry {
21
22
  moduleCache = new Map();
22
23
  githubRepoModule;
@@ -33,6 +34,9 @@ class RemoteSourceModuleRegistry {
33
34
  if (source.sourceType === "feishu_bot") {
34
35
  return Promise.resolve(FEISHU_BOT_MODULE);
35
36
  }
37
+ if (source.sourceType === "telegram_bot") {
38
+ return Promise.resolve(TELEGRAM_BOT_MODULE);
39
+ }
36
40
  if (source.sourceType !== "remote_source") {
37
41
  throw new Error(`unsupported source type for remote module: ${source.sourceType}`);
38
42
  }
@@ -79,6 +83,9 @@ function builtInModuleIdForSourceType(sourceType) {
79
83
  if (sourceType === "feishu_bot") {
80
84
  return "builtin.feishu_bot";
81
85
  }
86
+ if (sourceType === "telegram_bot") {
87
+ return "builtin.telegram_bot";
88
+ }
82
89
  return null;
83
90
  }
84
91
  function builtinRemoteSourceTypes() {
@@ -543,6 +550,87 @@ const FEISHU_BOT_MODULE = {
543
550
  };
544
551
  },
545
552
  };
553
+ const TELEGRAM_BOT_MODULE = {
554
+ id: "builtin.telegram_bot",
555
+ listDeliveryOperations(input) {
556
+ return (0, telegram_1.telegramDeliveryOperationsForHandle)(input.handle);
557
+ },
558
+ async invokeDeliveryOperation(input) {
559
+ return (0, telegram_1.invokeTelegramDeliveryOperation)(input.handle, input.operation, input.input);
560
+ },
561
+ describeCapabilities() {
562
+ return {
563
+ sourceKind: "telegram_bot",
564
+ aliases: ["telegram_bot"],
565
+ configSchema: [
566
+ { name: "botToken", type: "string", required: false, description: "Telegram bot token. Prefer tokenEnv for shared/local configuration." },
567
+ { name: "tokenEnv", type: "string", required: false, description: "Environment variable name containing the Telegram bot token." },
568
+ { name: "uxcAuth", type: "string", required: false, description: "Optional UXC Telegram auth profile." },
569
+ { name: "botUsername", type: "string", required: false, description: "Optional bot username used for source host identity." },
570
+ { name: "chatIds", type: "string[]", required: false, description: "Optional Telegram chat ID allowlist." },
571
+ { name: "allowedUpdates", type: "string[]", required: false, description: "Optional Telegram Bot API allowed_updates value." },
572
+ { name: "endpoint", type: "string", required: false, description: "Optional Telegram Bot API endpoint override." },
573
+ ],
574
+ metadataFields: [
575
+ { name: "updateId", type: "string", description: "Telegram update_id." },
576
+ { name: "chatId", type: "string", description: "Telegram chat ID." },
577
+ { name: "chatType", type: "string|null", description: "Telegram chat type such as private, group, or channel." },
578
+ { name: "messageId", type: "string", description: "Telegram message_id." },
579
+ { name: "messageType", type: "string", description: "Normalized Telegram message type such as text or photo." },
580
+ { name: "fromId", type: "string|null", description: "Telegram sender user ID when present." },
581
+ { name: "fromUsername", type: "string|null", description: "Telegram sender username when present." },
582
+ { name: "fromFirstName", type: "string|null", description: "Telegram sender first_name when present." },
583
+ { name: "content", type: "string|null", description: "Message text or caption when present." },
584
+ ],
585
+ eventVariantExamples: ["message.text", "edited_message.text", "channel_post.photo"],
586
+ };
587
+ },
588
+ validateConfig(source) {
589
+ (0, telegram_1.parseTelegramSourceConfig)(source);
590
+ },
591
+ buildManagedSourceSpec(source) {
592
+ const config = (0, telegram_1.parseTelegramSourceConfig)(source);
593
+ return {
594
+ endpoint: config.endpoint ?? telegram_1.TELEGRAM_BOT_API_ENDPOINT,
595
+ operation_id: "post:/getUpdates",
596
+ mode: "poll",
597
+ args: {
598
+ timeout: 5,
599
+ ...(config.allowedUpdates && config.allowedUpdates.length > 0 ? { allowed_updates: config.allowedUpdates } : {}),
600
+ },
601
+ poll_config: {
602
+ interval_secs: 2,
603
+ extract_items_pointer: "/result",
604
+ request_cursor_arg: "offset",
605
+ cursor_from_item_pointer: "/update_id",
606
+ cursor_transform: "increment",
607
+ checkpoint_strategy: {
608
+ type: "item_key",
609
+ item_key_pointer: "/update_id",
610
+ },
611
+ },
612
+ options: {
613
+ auth: config.uxcAuth,
614
+ artifact_compaction: false,
615
+ },
616
+ };
617
+ },
618
+ mapRawEvent(rawPayload, source) {
619
+ const config = (0, telegram_1.parseTelegramSourceConfig)(source);
620
+ const normalized = (0, telegram_1.normalizeTelegramBotUpdate)(source, config, rawPayload);
621
+ if (!normalized) {
622
+ return null;
623
+ }
624
+ return {
625
+ sourceNativeId: normalized.sourceNativeId,
626
+ eventVariant: normalized.eventVariant,
627
+ metadata: normalized.metadata ?? {},
628
+ rawPayload: normalized.rawPayload ?? rawPayload,
629
+ occurredAt: normalized.occurredAt,
630
+ deliveryHandle: normalized.deliveryHandle,
631
+ };
632
+ },
633
+ };
546
634
  function moduleConfigForSource(source) {
547
635
  const config = asRecord(source.config);
548
636
  if (source.sourceType === "remote_source") {
@@ -0,0 +1,318 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TelegramDeliveryAdapter = exports.TelegramBotApiClient = exports.TELEGRAM_BOT_API_ENDPOINT = void 0;
4
+ exports.telegramDeliveryOperationsForHandle = telegramDeliveryOperationsForHandle;
5
+ exports.invokeTelegramDeliveryOperation = invokeTelegramDeliveryOperation;
6
+ exports.normalizeTelegramBotUpdate = normalizeTelegramBotUpdate;
7
+ exports.parseTelegramSourceConfig = parseTelegramSourceConfig;
8
+ exports.TELEGRAM_BOT_API_ENDPOINT = "https://api.telegram.org";
9
+ class TelegramBotApiClient {
10
+ client;
11
+ constructor(client = { fetch: (url, init) => fetch(url, init) }) {
12
+ this.client = client;
13
+ }
14
+ async getUpdates(input) {
15
+ const endpoint = stripTrailingSlash(input.endpoint ?? exports.TELEGRAM_BOT_API_ENDPOINT);
16
+ const params = new URLSearchParams();
17
+ if (input.offset !== undefined) {
18
+ params.set("offset", String(input.offset));
19
+ }
20
+ if (input.timeout !== undefined) {
21
+ params.set("timeout", String(input.timeout));
22
+ }
23
+ if (input.allowedUpdates && input.allowedUpdates.length > 0) {
24
+ params.set("allowed_updates", JSON.stringify(input.allowedUpdates));
25
+ }
26
+ const query = params.toString();
27
+ const response = await this.client.fetch(`${endpoint}/bot${input.botToken}/getUpdates${query ? `?${query}` : ""}`);
28
+ const body = await response.text();
29
+ if (!response.ok) {
30
+ throw new Error(`Telegram getUpdates failed with status ${response.status}: ${body}`);
31
+ }
32
+ const parsed = JSON.parse(body);
33
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
34
+ throw new Error("Telegram getUpdates returned a non-object response");
35
+ }
36
+ const envelope = parsed;
37
+ if (envelope.ok !== true) {
38
+ throw new Error(`Telegram getUpdates returned ok=false: ${body}`);
39
+ }
40
+ const result = envelope.result;
41
+ if (!Array.isArray(result)) {
42
+ throw new Error("Telegram getUpdates response missing result array");
43
+ }
44
+ return result
45
+ .map((item) => asRecord(item))
46
+ .filter((item) => Object.keys(item).length > 0);
47
+ }
48
+ async sendMessage(input) {
49
+ const endpoint = stripTrailingSlash(input.endpoint ?? exports.TELEGRAM_BOT_API_ENDPOINT);
50
+ const body = {
51
+ chat_id: input.chatId,
52
+ text: input.text,
53
+ };
54
+ if (input.replyToMessageId !== undefined) {
55
+ body.reply_to_message_id = input.replyToMessageId;
56
+ }
57
+ if (input.parseMode) {
58
+ body.parse_mode = input.parseMode;
59
+ }
60
+ const response = await this.client.fetch(`${endpoint}/bot${input.botToken}/sendMessage`, {
61
+ method: "POST",
62
+ headers: { "content-type": "application/json" },
63
+ body: JSON.stringify(body),
64
+ });
65
+ if (!response.ok) {
66
+ throw new Error(`Telegram sendMessage failed with status ${response.status}: ${await response.text()}`);
67
+ }
68
+ }
69
+ }
70
+ exports.TelegramBotApiClient = TelegramBotApiClient;
71
+ class TelegramDeliveryAdapter {
72
+ client;
73
+ constructor(client) {
74
+ this.client = client ?? new TelegramBotApiClient();
75
+ }
76
+ async send(request, attempt) {
77
+ return invokeTelegramDeliveryOperation(attempt, "send_text", request.payload, this.client);
78
+ }
79
+ }
80
+ exports.TelegramDeliveryAdapter = TelegramDeliveryAdapter;
81
+ function telegramDeliveryOperationsForHandle(handle) {
82
+ if (handle.provider !== "telegram") {
83
+ return [];
84
+ }
85
+ if (handle.surface !== "message_reply" && handle.surface !== "chat_message") {
86
+ return [];
87
+ }
88
+ return [{
89
+ name: "send_text",
90
+ title: handle.surface === "message_reply" ? "Reply With Text" : "Send Text Message",
91
+ inputSchema: {
92
+ type: "object",
93
+ additionalProperties: false,
94
+ required: ["text"],
95
+ properties: {
96
+ text: { type: "string", minLength: 1 },
97
+ endpoint: { type: "string", minLength: 1 },
98
+ botToken: { type: "string", minLength: 1 },
99
+ token: { type: "string", minLength: 1 },
100
+ tokenEnv: { type: "string", minLength: 1 },
101
+ chatId: { type: "string", minLength: 1 },
102
+ chat_id: { type: "string", minLength: 1 },
103
+ replyToMessageId: { type: "number" },
104
+ reply_to_message_id: { type: "number" },
105
+ parseMode: { type: "string", minLength: 1 },
106
+ parse_mode: { type: "string", minLength: 1 },
107
+ },
108
+ },
109
+ canonicalTextAlias: true,
110
+ }];
111
+ }
112
+ async function invokeTelegramDeliveryOperation(handle, operation, input, client = new TelegramBotApiClient()) {
113
+ if (operation !== "send_text") {
114
+ throw new Error(`unknown Telegram delivery operation: ${operation}`);
115
+ }
116
+ if (handle.provider !== "telegram") {
117
+ throw new Error(`Telegram delivery operation requires a telegram handle, got ${handle.provider}`);
118
+ }
119
+ if (handle.surface !== "message_reply" && handle.surface !== "chat_message") {
120
+ throw new Error(`deliver send only supports canonical Telegram text surfaces; use deliver invoke for ${handle.surface}`);
121
+ }
122
+ const text = asString(input.text);
123
+ if (!text || text.trim().length === 0) {
124
+ throw new Error("send_text requires input.text");
125
+ }
126
+ const botToken = asString(input.botToken)
127
+ ?? asString(input.token)
128
+ ?? tokenFromEnv(asString(input.tokenEnv));
129
+ if (!botToken) {
130
+ throw new Error("send_text requires input.botToken, input.token, or input.tokenEnv");
131
+ }
132
+ const chatId = stringFromUnknown(input.chatId) ?? stringFromUnknown(input.chat_id) ?? handle.targetRef;
133
+ const replyToMessageId = numberFromUnknown(input.replyToMessageId)
134
+ ?? numberFromUnknown(input.reply_to_message_id)
135
+ ?? (handle.surface === "message_reply" ? numberFromUnknown(handle.threadRef) : undefined);
136
+ await client.sendMessage({
137
+ endpoint: asString(input.endpoint) ?? undefined,
138
+ botToken,
139
+ chatId,
140
+ text,
141
+ replyToMessageId,
142
+ parseMode: asString(input.parseMode) ?? asString(input.parse_mode) ?? undefined,
143
+ });
144
+ return {
145
+ status: "sent",
146
+ note: handle.surface === "message_reply" ? "sent Telegram message reply" : "sent Telegram chat message",
147
+ };
148
+ }
149
+ function normalizeTelegramBotUpdate(source, config, raw) {
150
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
151
+ return null;
152
+ }
153
+ const payload = raw;
154
+ const updateId = stringFromUnknown(payload.update_id);
155
+ if (!updateId) {
156
+ return null;
157
+ }
158
+ const selected = selectTelegramMessage(payload);
159
+ if (!selected) {
160
+ return null;
161
+ }
162
+ const message = selected.message;
163
+ const messageId = stringFromUnknown(message.message_id);
164
+ const chat = asRecord(message.chat);
165
+ const chatId = stringFromUnknown(chat.id);
166
+ if (!messageId || !chatId) {
167
+ return null;
168
+ }
169
+ if (config.chatIds && config.chatIds.length > 0 && !config.chatIds.includes(chatId)) {
170
+ return null;
171
+ }
172
+ const from = asRecord(message.from);
173
+ const content = asString(message.text) ?? asString(message.caption);
174
+ const messageType = telegramMessageType(message);
175
+ return {
176
+ sourceId: source.sourceId,
177
+ sourceNativeId: `telegram:${source.sourceId}:${updateId}`,
178
+ eventVariant: `${selected.kind}.${messageType}`,
179
+ occurredAt: fromUnixSeconds(message.date) ?? new Date().toISOString(),
180
+ metadata: {
181
+ provider: "telegram",
182
+ updateId,
183
+ chatId,
184
+ chatType: asString(chat.type),
185
+ messageId,
186
+ messageType,
187
+ fromId: stringFromUnknown(from.id),
188
+ fromUsername: asString(from.username),
189
+ fromFirstName: asString(from.first_name),
190
+ content,
191
+ },
192
+ rawPayload: payload,
193
+ deliveryHandle: {
194
+ provider: "telegram",
195
+ surface: "message_reply",
196
+ targetRef: chatId,
197
+ threadRef: messageId,
198
+ replyMode: "reply",
199
+ },
200
+ };
201
+ }
202
+ function parseTelegramSourceConfig(source) {
203
+ const config = source.config ?? {};
204
+ return {
205
+ endpoint: asString(config.endpoint) ?? asString(config.apiBaseUrl) ?? exports.TELEGRAM_BOT_API_ENDPOINT,
206
+ uxcAuth: asString(config.uxcAuth) ?? asString(config.credentialRef) ?? undefined,
207
+ botToken: asString(config.botToken) ?? undefined,
208
+ tokenEnv: asString(config.tokenEnv) ?? undefined,
209
+ botUsername: asString(config.botUsername) ?? undefined,
210
+ chatIds: asStringArray(config.chatIds) ?? undefined,
211
+ allowedUpdates: asStringArray(config.allowedUpdates) ?? undefined,
212
+ };
213
+ }
214
+ function selectTelegramMessage(payload) {
215
+ const candidates = [
216
+ ["message", payload.message],
217
+ ["edited_message", payload.edited_message],
218
+ ["channel_post", payload.channel_post],
219
+ ["edited_channel_post", payload.edited_channel_post],
220
+ ];
221
+ for (const [kind, value] of candidates) {
222
+ const message = asRecord(value);
223
+ if (Object.keys(message).length > 0) {
224
+ return { kind, message };
225
+ }
226
+ }
227
+ return null;
228
+ }
229
+ function telegramMessageType(message) {
230
+ if (typeof message.text === "string") {
231
+ return "text";
232
+ }
233
+ if (typeof message.caption === "string") {
234
+ return "caption";
235
+ }
236
+ if (Array.isArray(message.photo)) {
237
+ return "photo";
238
+ }
239
+ if (message.document && typeof message.document === "object") {
240
+ return "document";
241
+ }
242
+ if (message.audio && typeof message.audio === "object") {
243
+ return "audio";
244
+ }
245
+ if (message.video && typeof message.video === "object") {
246
+ return "video";
247
+ }
248
+ if (message.voice && typeof message.voice === "object") {
249
+ return "voice";
250
+ }
251
+ if (message.sticker && typeof message.sticker === "object") {
252
+ return "sticker";
253
+ }
254
+ return "unknown";
255
+ }
256
+ function fromUnixSeconds(value) {
257
+ const seconds = numberFromUnknown(value);
258
+ if (seconds === undefined) {
259
+ return null;
260
+ }
261
+ return new Date(seconds * 1000).toISOString();
262
+ }
263
+ function tokenFromEnv(name) {
264
+ if (!name) {
265
+ return null;
266
+ }
267
+ return asString(process.env[name]);
268
+ }
269
+ function stripTrailingSlash(value) {
270
+ return value.replace(/\/+$/, "");
271
+ }
272
+ function asRecord(value) {
273
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
274
+ return {};
275
+ }
276
+ return value;
277
+ }
278
+ function asString(value) {
279
+ return typeof value === "string" ? value : null;
280
+ }
281
+ function asStringArray(value) {
282
+ if (Array.isArray(value)) {
283
+ return value
284
+ .map((item) => stringFromUnknown(item))
285
+ .filter((item) => Boolean(item));
286
+ }
287
+ if (typeof value === "string") {
288
+ return value
289
+ .split(",")
290
+ .map((item) => item.trim())
291
+ .filter((item) => item.length > 0);
292
+ }
293
+ return null;
294
+ }
295
+ function stringFromUnknown(value) {
296
+ if (typeof value === "string") {
297
+ return value;
298
+ }
299
+ if (typeof value === "number" && Number.isFinite(value)) {
300
+ return String(value);
301
+ }
302
+ if (typeof value === "bigint") {
303
+ return value.toString();
304
+ }
305
+ return null;
306
+ }
307
+ function numberFromUnknown(value) {
308
+ if (typeof value === "number" && Number.isFinite(value)) {
309
+ return value;
310
+ }
311
+ if (typeof value === "string" && value.trim().length > 0) {
312
+ const parsed = Number(value);
313
+ if (Number.isFinite(parsed)) {
314
+ return parsed;
315
+ }
316
+ }
317
+ return undefined;
318
+ }
package/dist/src/store.js CHANGED
@@ -7,6 +7,7 @@ exports.AgentInboxStore = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
10
+ const store_queries_1 = require("./store_queries");
10
11
  const util_1 = require("./util");
11
12
  const DRIZZLE_MIGRATIONS_TABLE = "__drizzle_migrations";
12
13
  const V1_BASELINE_TAG = "0000_v1_initial";
@@ -130,6 +131,13 @@ class AgentInboxStore {
130
131
  save() {
131
132
  this.persist();
132
133
  }
134
+ getDatabaseHealth() {
135
+ return {
136
+ integrityCheck: String(this.db.pragma("integrity_check", { simple: true })),
137
+ journalMode: String(this.db.pragma("journal_mode", { simple: true })),
138
+ foreignKeys: Number(this.db.pragma("foreign_keys", { simple: true })) === 1,
139
+ };
140
+ }
133
141
  async backupHealthyDatabase() {
134
142
  const backupPath = `${this.dbPath}.bak`;
135
143
  const tmpPath = `${backupPath}.${process.pid}.tmp`;
@@ -667,40 +675,23 @@ class AgentInboxStore {
667
675
  this.persist();
668
676
  }
669
677
  listSubscriptions(limit) {
670
- const rows = typeof limit === "number"
671
- ? this.getAll("select * from subscriptions order by created_at asc, subscription_id asc limit ?", [limit])
672
- : this.getAll("select * from subscriptions order by created_at asc, subscription_id asc");
678
+ const query = (0, store_queries_1.buildSubscriptionListQuery)({ limit });
679
+ const rows = this.getAll(query.sql, query.params);
673
680
  return rows.map((row) => this.mapSubscription(row));
674
681
  }
675
682
  listSubscriptionsFiltered(filters) {
676
- const clauses = [];
677
- const params = [];
678
- if (filters?.sourceId) {
679
- clauses.push("source_id = ?");
680
- params.push(filters.sourceId);
681
- }
682
- if (filters?.agentId) {
683
- clauses.push("agent_id = ?");
684
- params.push(filters.agentId);
685
- }
686
- const where = clauses.length > 0 ? ` where ${clauses.join(" and ")}` : "";
687
- const limitClause = typeof filters?.limit === "number" ? " limit ?" : "";
688
- if (typeof filters?.limit === "number") {
689
- params.push(filters.limit);
690
- }
691
- const rows = this.getAll(`select * from subscriptions${where} order by created_at asc, subscription_id asc${limitClause}`, params);
683
+ const query = (0, store_queries_1.buildSubscriptionListQuery)(filters);
684
+ const rows = this.getAll(query.sql, query.params);
692
685
  return rows.map((row) => this.mapSubscription(row));
693
686
  }
694
687
  listSubscriptionsForSource(sourceId, limit) {
695
- const rows = typeof limit === "number"
696
- ? this.getAll("select * from subscriptions where source_id = ? order by created_at asc, subscription_id asc limit ?", [sourceId, limit])
697
- : this.getAll("select * from subscriptions where source_id = ? order by created_at asc, subscription_id asc", [sourceId]);
688
+ const query = (0, store_queries_1.buildSubscriptionListQuery)({ sourceId, limit });
689
+ const rows = this.getAll(query.sql, query.params);
698
690
  return rows.map((row) => this.mapSubscription(row));
699
691
  }
700
692
  listSubscriptionsForAgent(agentId, limit) {
701
- const rows = typeof limit === "number"
702
- ? this.getAll("select * from subscriptions where agent_id = ? order by created_at asc, subscription_id asc limit ?", [agentId, limit])
703
- : this.getAll("select * from subscriptions where agent_id = ? order by created_at asc, subscription_id asc", [agentId]);
693
+ const query = (0, store_queries_1.buildSubscriptionListQuery)({ agentId, limit });
694
+ const rows = this.getAll(query.sql, query.params);
704
695
  return rows.map((row) => this.mapSubscription(row));
705
696
  }
706
697
  listSubscriptionsForHostTrackedResourceRef(hostId, trackedResourceRef) {
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildSubscriptionListQuery = buildSubscriptionListQuery;
4
+ function buildSubscriptionListQuery(filters) {
5
+ const clauses = [];
6
+ const params = [];
7
+ if (filters?.sourceId) {
8
+ clauses.push("source_id = ?");
9
+ params.push(filters.sourceId);
10
+ }
11
+ if (filters?.agentId) {
12
+ clauses.push("agent_id = ?");
13
+ params.push(filters.agentId);
14
+ }
15
+ const where = clauses.length > 0 ? ` where ${clauses.join(" and ")}` : "";
16
+ const limitClause = typeof filters?.limit === "number" ? " limit ?" : "";
17
+ if (typeof filters?.limit === "number") {
18
+ params.push(filters.limit);
19
+ }
20
+ return {
21
+ sql: `select * from subscriptions${where} order by created_at asc, subscription_id asc${limitClause}`,
22
+ params,
23
+ };
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holon-run/agentinbox",
3
- "version": "1.3.3",
3
+ "version": "1.4.1",
4
4
  "description": "Local event subscription and delivery service for agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@fastify/swagger": "^9.5.2",
47
- "@holon-run/uxc-daemon-client": "^0.15.4",
47
+ "@holon-run/uxc-daemon-client": "^0.15.8",
48
48
  "better-sqlite3": "^12.10.0",
49
49
  "commander": "^14.0.3",
50
50
  "fastify": "^5.6.1",