@goodandready/dsh-messenger-gateway 0.3.18 → 0.3.20

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.
package/lib/stream.js CHANGED
@@ -1,101 +1,114 @@
1
- /** Helpers for Telegram streaming edits + progress. */
2
-
3
- export function extractTextDelta(chunk) {
4
- if (!chunk || typeof chunk !== 'object') return ''
5
- if (chunk.type === 'text-delta' && typeof chunk.text === 'string') return chunk.text
6
- return ''
7
- }
8
-
9
- export function extractToolName(eventData) {
10
- const name = eventData?.tool?.name || eventData?.toolName || eventData?.name
11
- return name ? String(name) : ''
12
- }
13
-
14
- export function formatProgressLine(toolName) {
15
- if (!toolName) return '⏳ Думаю…'
16
- return `🔧 ${toolName}…`
17
- }
18
-
19
- export function buildStreamPreview(streamText, toolName, maxLen = 3500) {
20
- const body = String(streamText || '').trimEnd()
21
- const head = toolName ? `${formatProgressLine(toolName)}\n\n` : ''
22
- const combined = head + (body || '…')
23
- if (combined.length <= maxLen) return combined
24
- return combined.slice(0, maxLen - 1) + '…'
25
- }
26
-
27
- export function parseRetryAfter(err) {
28
- if (!err) return 0
29
- const msg = err.message || String(err)
30
- const match = /retry after (\d+)/i.exec(msg)
31
- if (match) return parseInt(match[1], 10) * 1000
32
- return 0
33
- }
34
-
35
- export function createEditScheduler(editFn, intervalMs = 1200) {
36
- let pending = null
37
- let timer = null
38
- let lastSent = ''
39
- let inFlight = Promise.resolve()
40
- let retryDelayMs = 0
41
-
42
- const flush = () => {
43
- timer = null
44
- if (pending === null || pending === lastSent) return
45
- const text = pending
46
- inFlight = inFlight.then(async () => {
47
- try {
48
- if (retryDelayMs > 0) {
49
- const delay = retryDelayMs
50
- retryDelayMs = 0
51
- await new Promise((r) => setTimeout(r, delay))
52
- }
53
- await editFn(text)
54
- lastSent = text
55
- } catch (err) {
56
- const retry = parseRetryAfter(err)
57
- if (retry > 0) {
58
- retryDelayMs = retry
59
- // Re-schedule flush with retry delay
60
- if (!timer) {
61
- timer = setTimeout(flush, retry + 100)
62
- timer.unref?.()
63
- }
64
- }
65
- }
66
- })
67
- }
68
-
69
- return {
70
- push(text) {
71
- pending = text
72
- if (timer) return
73
- timer = setTimeout(flush, Math.max(200, intervalMs))
74
- timer.unref?.()
75
- },
76
- async flush() {
77
- if (timer) { clearTimeout(timer); timer = null }
78
- flush()
79
- await inFlight
80
- // If there's still a pending difference (e.g. rate limit delay happened), wait and retry
81
- if (pending !== null && pending !== lastSent) {
82
- if (retryDelayMs > 0) {
83
- await new Promise((r) => setTimeout(r, retryDelayMs + 50))
84
- retryDelayMs = 0
85
- }
86
- try {
87
- await editFn(pending)
88
- lastSent = pending
89
- } catch {}
90
- }
91
- },
92
- }
93
- }
94
-
95
- export function startTypingHeartbeat(typingFn, intervalMs = 4000) {
96
- if (typeof typingFn !== 'function') return () => {}
97
- typingFn().catch?.(() => {})
98
- const timer = setInterval(() => { typingFn().catch?.(() => {}) }, intervalMs)
99
- timer.unref?.()
100
- return () => clearInterval(timer)
101
- }
1
+ /** Helpers for Telegram streaming edits + progress. */
2
+
3
+ export function extractTextDelta(chunk) {
4
+ if (!chunk || typeof chunk !== 'object') return ''
5
+ if (chunk.type === 'text-delta' && typeof chunk.text === 'string') return chunk.text
6
+ return ''
7
+ }
8
+
9
+ export function extractToolName(eventData) {
10
+ const name = eventData?.tool?.name || eventData?.toolName || eventData?.name
11
+ return name ? String(name) : ''
12
+ }
13
+
14
+ export function formatProgressLine(toolName) {
15
+ if (!toolName) return '⏳ Думаю…'
16
+ return `🔧 ${toolName}…`
17
+ }
18
+
19
+ export function buildStreamPreview(streamText, toolName, maxLen = 3500) {
20
+ const body = String(streamText || '').trimEnd()
21
+ const head = toolName ? `${formatProgressLine(toolName)}\n\n` : ''
22
+ const combined = head + (body || '…')
23
+ if (combined.length <= maxLen) return combined
24
+ return combined.slice(0, maxLen - 1) + '…'
25
+ }
26
+
27
+ export function parseRetryAfter(err) {
28
+ if (!err) return 0
29
+ const msg = err.message || String(err)
30
+ const match = /retry after (\d+)/i.exec(msg)
31
+ if (match) return parseInt(match[1], 10) * 1000
32
+ return 0
33
+ }
34
+
35
+ export function createEditScheduler(editFn, intervalMs = 1200, firstChunkDelayMs = 250) {
36
+ let pending = null
37
+ let timer = null
38
+ let lastSent = ''
39
+ let inFlight = Promise.resolve()
40
+ let retryDelayMs = 0
41
+
42
+ const flush = () => {
43
+ timer = null
44
+ if (pending === null || pending === lastSent) return
45
+ const text = pending
46
+ inFlight = inFlight.then(async () => {
47
+ try {
48
+ if (retryDelayMs > 0) {
49
+ const delay = retryDelayMs
50
+ retryDelayMs = 0
51
+ await new Promise((r) => setTimeout(r, delay))
52
+ }
53
+ await editFn(text)
54
+ lastSent = text
55
+ } catch (err) {
56
+ const retry = parseRetryAfter(err)
57
+ if (retry > 0) {
58
+ retryDelayMs = retry
59
+ // Re-schedule flush with retry delay
60
+ if (!timer) {
61
+ timer = setTimeout(flush, retry + 100)
62
+ timer.unref?.()
63
+ }
64
+ }
65
+ }
66
+ })
67
+ }
68
+
69
+ return {
70
+ push(text) {
71
+ pending = text
72
+ if (timer) return
73
+ // Fast first chunk: when lastSent is empty, dispatch quickly so user sees immediate response.
74
+ // Subsequent edits throttle to intervalMs to respect Telegram rate limits.
75
+ const delay = !lastSent ? Math.min(firstChunkDelayMs, intervalMs) : Math.max(200, intervalMs)
76
+ timer = setTimeout(flush, delay)
77
+ timer.unref?.()
78
+ },
79
+ async flush() {
80
+ if (timer) { clearTimeout(timer); timer = null }
81
+ flush()
82
+ await inFlight
83
+ // If there's still a pending difference (e.g. rate limit delay happened), wait and retry
84
+ if (pending !== null && pending !== lastSent) {
85
+ if (retryDelayMs > 0) {
86
+ await new Promise((r) => setTimeout(r, retryDelayMs + 50))
87
+ retryDelayMs = 0
88
+ }
89
+ try {
90
+ await editFn(pending)
91
+ lastSent = pending
92
+ } catch {}
93
+ }
94
+ },
95
+ }
96
+ }
97
+
98
+ export function startTypingHeartbeat(typingFn, intervalMs = 4000) {
99
+ if (typeof typingFn !== 'function') return () => {}
100
+ let active = true
101
+ const safeInvoke = () => {
102
+ if (!active) return
103
+ try {
104
+ typingFn()?.catch?.(() => {})
105
+ } catch {}
106
+ }
107
+ safeInvoke()
108
+ const timer = setInterval(safeInvoke, intervalMs)
109
+ timer.unref?.()
110
+ return () => {
111
+ active = false
112
+ clearInterval(timer)
113
+ }
114
+ }
@@ -1,51 +1,51 @@
1
- // Network error classification for Telegram sends (mirrors Hermes adapter policy).
2
- //
3
- // The key distinction: some network failures mean the request NEVER left the
4
- // process (connect/pool timeout, ECONNRESET before send) → resending is safe.
5
- // Others (a generic timeout after the request may have reached Telegram) could
6
- // duplicate a message if we resend → do NOT retry.
7
-
8
- function rootCause(err) {
9
- let e = err
10
- let depth = 0
11
- while (e && e.cause && e.cause !== e && depth < 10) {
12
- e = e.cause
13
- depth++
14
- }
15
- return e || err
16
- }
17
-
18
- // True when a resend cannot duplicate a message: the request did not reach Telegram.
19
- export function isResendSafeNetworkError(err) {
20
- const c = rootCause(err)
21
- const msg = String(c?.message || err?.message || '').toLowerCase()
22
- if (/not sent to telegram|connect timeout|und_err_connect|econnreset|enotfound|econnrefused|ECONNRESET|ENOTFOUND|ECONNREFUSED/i.test(msg)) {
23
- return true
24
- }
25
- // undici PoolTimeout message: "Request was *not* sent to Telegram."
26
- if (/pool timeout|request was \*?not\*? sent/i.test(msg)) return true
27
- return false
28
- }
29
-
30
- // True for a 409 from getUpdates: a second bot instance polls the same token.
31
- export function isPollingConflict(err) {
32
- const msg = String(err?.message || '').toLowerCase()
33
- return /terminated by other getupdates request|another bot instance is running|getupdates.*conflict|conflict.*getupdates/i.test(msg)
34
- }
35
-
36
- // True when the target chat/topic no longer exists (deleted/closed/upgraded).
37
- // Such sends should not be retried and any pending ask binding must be pruned.
38
- export function isTopicGoneError(err) {
39
- const msg = String(err?.message || '').toLowerCase()
40
- return /thread not found|message thread not found|topic[_ ]?(not found|closed|deleted)|chat (not found|was (upgraded|deleted))|group chat was (upgraded|deleted)/i.test(msg)
41
- }
42
-
43
- /**
44
- * Calculates exponential backoff with max cap for polling retries.
45
- * errorCount is 1-indexed.
46
- */
47
- export function computePollBackoffMs(baseIntervalMs, errorCount, maxDelayMs = 30_000) {
48
- const count = Math.max(1, Math.min(10, Number(errorCount) || 1))
49
- const base = Math.max(100, Number(baseIntervalMs) || 500)
50
- return Math.min(maxDelayMs, base * Math.pow(2, count - 1))
51
- }
1
+ // Network error classification for Telegram sends (mirrors Hermes adapter policy).
2
+ //
3
+ // The key distinction: some network failures mean the request NEVER left the
4
+ // process (connect/pool timeout, ECONNRESET before send) → resending is safe.
5
+ // Others (a generic timeout after the request may have reached Telegram) could
6
+ // duplicate a message if we resend → do NOT retry.
7
+
8
+ function rootCause(err) {
9
+ let e = err
10
+ let depth = 0
11
+ while (e && e.cause && e.cause !== e && depth < 10) {
12
+ e = e.cause
13
+ depth++
14
+ }
15
+ return e || err
16
+ }
17
+
18
+ // True when a resend cannot duplicate a message: the request did not reach Telegram.
19
+ export function isResendSafeNetworkError(err) {
20
+ const c = rootCause(err)
21
+ const msg = String(c?.message || err?.message || '').toLowerCase()
22
+ if (/not sent to telegram|connect timeout|und_err_connect|econnreset|enotfound|econnrefused|ECONNRESET|ENOTFOUND|ECONNREFUSED/i.test(msg)) {
23
+ return true
24
+ }
25
+ // undici PoolTimeout message: "Request was *not* sent to Telegram."
26
+ if (/pool timeout|request was \*?not\*? sent/i.test(msg)) return true
27
+ return false
28
+ }
29
+
30
+ // True for a 409 from getUpdates: a second bot instance polls the same token.
31
+ export function isPollingConflict(err) {
32
+ const msg = String(err?.message || '').toLowerCase()
33
+ return /terminated by other getupdates request|another bot instance is running|getupdates.*conflict|conflict.*getupdates/i.test(msg)
34
+ }
35
+
36
+ // True when the target chat/topic no longer exists (deleted/closed/upgraded).
37
+ // Such sends should not be retried and any pending ask binding must be pruned.
38
+ export function isTopicGoneError(err) {
39
+ const msg = String(err?.message || '').toLowerCase()
40
+ return /thread not found|message thread not found|topic[_ ]?(not found|closed|deleted)|chat (not found|was (upgraded|deleted))|group chat was (upgraded|deleted)/i.test(msg)
41
+ }
42
+
43
+ /**
44
+ * Calculates exponential backoff with max cap for polling retries.
45
+ * errorCount is 1-indexed.
46
+ */
47
+ export function computePollBackoffMs(baseIntervalMs, errorCount, maxDelayMs = 30_000) {
48
+ const count = Math.max(1, Math.min(10, Number(errorCount) || 1))
49
+ const base = Math.max(100, Number(baseIntervalMs) || 500)
50
+ return Math.min(maxDelayMs, base * Math.pow(2, count - 1))
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-messenger-gateway",
3
- "version": "0.3.18",
3
+ "version": "0.3.20",
4
4
  "description": "Telegram messenger bridge for DeepSeek Harness: sessions, steer, homes, inline asks, notify bridge, and optional TTS voice notes.",
5
5
  "license": "MIT",
6
6
  "type": "module",