polygram 0.5.5 → 0.5.7

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.
@@ -6,14 +6,17 @@
6
6
  * interrupt the in-flight turn instead of queueing the message behind it.
7
7
  *
8
8
  * Conservative on purpose. False positives hijack user intent — "stop using
9
- * emoji" should NOT abort. So we require:
10
- * 1. The message (after stripping leading @-mention + trailing punctuation)
11
- * must be an exact match against a known abort phrase, OR
12
- * 2. It must start with an explicit slash command: /stop, /abort, /cancel.
9
+ * emoji" should NOT abort. So we require ONE of:
10
+ * 1. The whole message (after stripping leading @-mention + trailing
11
+ * punctuation) is an exact match against a known abort phrase, OR
12
+ * 2. It starts with an explicit slash command: /stop, /abort, /cancel, OR
13
+ * 3. The FIRST SENTENCE (split on . ! ?) is an exact abort phrase. This
14
+ * catches "Stop. I'll ask in another session." — clear abort intent
15
+ * with continuation explaining what comes next. Comma is not a split
16
+ * character ("Stop, look here" is ambiguous and stays non-abort).
13
17
  *
14
18
  * Not detected (on purpose):
15
- * - "wait a sec while I finish typing" too long, real content
16
- * - "stop using markdown" → has trailing content
19
+ * - "stop using markdown" first sentence is the whole thing, not exact
17
20
  * - "I said stop" → not at start / not exact match
18
21
  */
19
22
 
@@ -54,10 +57,21 @@ function isAbortRequest(text) {
54
57
 
55
58
  const n = normalize(text);
56
59
  if (!n) return false;
57
- // Cap length: a long message that happens to start with "stop" is real
58
- // content, not an abort. 40 chars covers all phrases above with headroom.
59
- if (n.length > 40) return false;
60
- return ABORT_PHRASES.has(n);
60
+ // Whole-message exact match (capped — a long message that happens to
61
+ // start with "stop" is real content, not an abort).
62
+ if (n.length <= 40 && ABORT_PHRASES.has(n)) return true;
63
+
64
+ // First-sentence exact match. Splits on . ! ? (NOT comma — "Stop, look
65
+ // here" is ambiguous and stays non-abort). The leading @-mention has
66
+ // already been stripped by normalize but only on the whole string, so
67
+ // we strip it again on the raw text before splitting.
68
+ const head = text.trim().replace(LEADING_MENTION_RE, '');
69
+ const firstSentence = head.split(/[.!?]/, 1)[0]?.trim().toLowerCase();
70
+ if (firstSentence && firstSentence.length <= 40 && ABORT_PHRASES.has(firstSentence)) {
71
+ return true;
72
+ }
73
+
74
+ return false;
61
75
  }
62
76
 
63
77
  module.exports = { isAbortRequest, ABORT_PHRASES };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Telegram daemon for Claude Code that preserves the OpenClaw per-chat session model. Migration path for OpenClaw users moving to Claude Code.",
5
5
  "main": "lib/ipc-client.js",
6
6
  "bin": {
package/polygram.js CHANGED
@@ -576,12 +576,32 @@ const inFlightHandlers = new Map(); // sessionKey → count
576
576
  // polygram is going down, in-flight handlers reject with "Process
577
577
  // killed" / "Process exited" but those failures aren't "real" — the
578
578
  // next boot's replay will re-dispatch them. Suppressing the user-facing
579
- // "Sorry, I couldn't process" during shutdown removes a misleading
580
- // post-mortem apology that the user shouldn't have seen in the first
581
- // place. (The boot replay's own _isReplay flag handles the OTHER half:
582
- // suppressing the apology if the replay itself fails.)
579
+ // error reply during shutdown removes a misleading post-mortem apology
580
+ // the user shouldn't see. (The boot replay's own _isReplay flag handles
581
+ // the OTHER half: suppressing if the replay itself fails.)
583
582
  let isShuttingDown = false;
584
583
 
584
+ // Map a handler-error to a user-facing reply that says what happened
585
+ // and what to do next. The technical strings come from process-manager
586
+ // (idle / wall-clock timeouts) and node child_process (Process exited /
587
+ // killed). Anything we don't recognise falls back to a generic line
588
+ // with a single-line snippet of the error so the user can at least
589
+ // distinguish unique failures from the obvious "try again" cases.
590
+ function errorReplyText(err) {
591
+ const msg = err?.message || '';
592
+ if (/idle with no Claude activity/i.test(msg)) {
593
+ return '⏳ I went quiet too long without finishing. Try resending or simplifying the task.';
594
+ }
595
+ if (/wall-clock ceiling/i.test(msg)) {
596
+ return '⏱ This was taking too long, so I stopped. Try resending or simplifying the task.';
597
+ }
598
+ if (/Process (exited|killed)/i.test(msg)) {
599
+ return '💥 Something crashed on my end. Try again.';
600
+ }
601
+ const reason = msg.split('\n')[0].slice(0, 120);
602
+ return `Hit a snag: ${reason || 'unknown error'}. Try resending.`;
603
+ }
604
+
585
605
  // Sessions the operator just /stop'd (or natural-language "стоп"). Keyed
586
606
  // by sessionKey → timestamp of abort. ANY pending that rejects within
587
607
  // ABORT_GRACE_MS of the mark is considered abort-caused — its generic
@@ -638,7 +658,7 @@ function dispatchHandleMessage(sessionKey, chatId, msg, bot) {
638
658
  aborted: wasAborted || undefined,
639
659
  replay: isReplay || undefined,
640
660
  }), 'log handler-error');
641
- // Suppress the "Sorry, I couldn't process" reply when:
661
+ // Suppress the user-facing error reply when:
642
662
  // - boot replay (user typed this minutes ago and moved on)
643
663
  // - polygram is shutting down (the failure is "Process killed" /
644
664
  // "Process exited" which isn't a real error — boot replay will
@@ -647,7 +667,7 @@ function dispatchHandleMessage(sessionKey, chatId, msg, bot) {
647
667
  if (!wasAborted && !isReplay && !isShuttingDown) {
648
668
  tg(bot, 'sendMessage', {
649
669
  chat_id: chatId,
650
- text: `Sorry, I couldn't process that message. The operator has been notified.`,
670
+ text: errorReplyText(err),
651
671
  reply_parameters: { message_id: msg.message_id },
652
672
  }, { source: 'error-reply', botName: BOT_NAME }).catch((replyErr) => {
653
673
  console.error(`[${sessionKey}] failed to send error reply: ${replyErr.message}`);