ping-a-human 0.1.1 → 0.1.3

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.
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Telegram bot commands (text beginning with "/", e.g. "/start", "/help") are
3
+ * client/protocol messages, never a human's answer to a free-text question.
4
+ * The most common offender is the "/start" Telegram auto-sends when a user
5
+ * first opens the bot, which would otherwise be returned as the reply.
6
+ */
7
+ function isBotCommand(text) {
8
+ return /^\/[A-Za-z0-9_]+(@\w+)?(\s|$)/.test(text.trim());
9
+ }
1
10
  /**
2
11
  * Telegram implementation of the Channel interface using the Bot API over
3
12
  * plain HTTPS (Node's built-in fetch). No third-party Telegram SDK.
@@ -58,6 +67,15 @@ export class TelegramChannel {
58
67
  }
59
68
  async awaitReply(options) {
60
69
  const deadline = Date.now() + options.timeoutMs;
70
+ // Anchor: only accept replies that arrive AFTER the question was sent.
71
+ // Telegram message ids are monotonically increasing per chat, so any
72
+ // update whose message predates the question (e.g. a queued `/start` from
73
+ // first opening the bot) must be ignored — otherwise it would be wrongly
74
+ // returned as the human's answer. Honors AwaitReplyOptions.sinceRef.
75
+ const sinceMessageId = options.sinceRef != null ? Number(options.sinceRef.id) : undefined;
76
+ const isStale = (messageId) => sinceMessageId != null &&
77
+ messageId != null &&
78
+ messageId <= sinceMessageId;
61
79
  while (Date.now() < deadline) {
62
80
  const remainingMs = deadline - Date.now();
63
81
  // Don't long-poll longer than the time we have left.
@@ -73,6 +91,13 @@ export class TelegramChannel {
73
91
  // Free-text reply.
74
92
  const msg = update.message;
75
93
  if (msg?.text && this.fromConfiguredChat(msg.chat)) {
94
+ // Skip backlog that predates the question (e.g. a stale `/start`).
95
+ if (isStale(msg.message_id))
96
+ continue;
97
+ // Bot commands ("/start", "/help", ...) are never valid answers to a
98
+ // free-text question; treat them as noise and keep waiting.
99
+ if (isBotCommand(msg.text))
100
+ continue;
76
101
  return {
77
102
  status: "answered",
78
103
  answer: msg.text,
@@ -81,7 +106,7 @@ export class TelegramChannel {
81
106
  }
82
107
  // Inline-button tap.
83
108
  const cb = update.callback_query;
84
- if (cb) {
109
+ if (cb && !isStale(cb.message?.message_id)) {
85
110
  // Best-effort: clear the client's loading spinner.
86
111
  try {
87
112
  await this.api("answerCallbackQuery", { callback_query_id: cb.id });
package/dist/index.js CHANGED
@@ -21,12 +21,12 @@ export function createServer(channelOptions = {}) {
21
21
  // 1.x registerTool: inputSchema is a ZodRawShape (plain object), NOT z.object(...).
22
22
  server.registerTool("notify_human", {
23
23
  title: "Notify human",
24
- description: "Send a fire-and-forget message to the configured human (via Telegram) and return immediately without waiting for a reply.",
24
+ description: "Send a one-way, fire-and-forget message to the configured human (via Telegram) and return IMMEDIATELY. Use this ONLY to inform the human (status updates, 'task finished', 'deploy succeeded', FYIs) when you do NOT need anything back. The human's reply, if any, is NOT captured or returned. If you need a decision, approval, or any answer before continuing, DO NOT use this — use ask_human instead.",
25
25
  inputSchema: { message: z.string() },
26
26
  }, async ({ message }) => notifyHuman(resolveChannel(), { message }));
27
27
  server.registerTool("ask_human", {
28
28
  title: "Ask human",
29
- description: "Send a question to the configured human and block until they reply (or a timeout elapses). Optionally provide choices to render tappable buttons. Returns the human's answer, or a clear timed-out result.",
29
+ description: "Ask the configured human a question and BLOCK until they reply on their messaging app (Telegram) or a timeout elapses. Use this whenever you need a human decision, approval, confirmation, clarification, or any answer before you can continue — the human's reply is captured and returned to you. Optionally provide `choices` to render tappable buttons (the tapped value is returned). Returns the human's answer, or a clear timed-out result if they don't respond in time. If you only need to inform the human and do NOT need a response, use notify_human instead.",
30
30
  inputSchema: {
31
31
  question: z.string(),
32
32
  choices: z.array(z.string()).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ping-a-human",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "An MCP server that adds a human-in-the-loop step to any AI pipeline by reaching a human on their own messaging app (Telegram first).",
5
5
  "type": "module",
6
6
  "bin": {