@goodandready/dsh-messenger-gateway 0.3.13 → 0.3.18

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,41 +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
- }
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,5 +1,19 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, unlinkSync } from "node:fs"
2
2
  import { dirname } from "node:path"
3
+ import { randomUUID } from "node:crypto"
4
+
5
+ function writeJsonAtomicSync(filePath, data) {
6
+ const dir = dirname(filePath)
7
+ mkdirSync(dir, { recursive: true })
8
+ const tmpPath = `${filePath}.${randomUUID().slice(0, 8)}.tmp`
9
+ try {
10
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf8")
11
+ renameSync(tmpPath, filePath)
12
+ } catch (err) {
13
+ try { unlinkSync(tmpPath) } catch {}
14
+ throw err
15
+ }
16
+ }
3
17
 
4
18
  export function createVoicePrefs(filePath) {
5
19
  /** @type {Record<string, boolean>} */
@@ -12,8 +26,9 @@ export function createVoicePrefs(filePath) {
12
26
  }
13
27
  const persist = () => {
14
28
  if (!filePath) return
15
- mkdirSync(dirname(filePath), { recursive: true })
16
- writeFileSync(filePath, JSON.stringify(state, null, 2))
29
+ try {
30
+ writeJsonAtomicSync(filePath, state)
31
+ } catch {}
17
32
  }
18
33
  const key = (userId) => String(Number(userId) || userId || "")
19
34
  return {
@@ -43,4 +58,4 @@ export function shouldSpeakReply({ globalTts, voiceMode, inboundWasVoice, userPr
43
58
  if (voiceMode === "off") return false
44
59
  // mirror: speak if inbound was voice
45
60
  return Boolean(inboundWasVoice)
46
- }
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-messenger-gateway",
3
- "version": "0.3.13",
3
+ "version": "0.3.18",
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",
@@ -42,10 +42,7 @@
42
42
  },
43
43
  "client": {
44
44
  "platform": "web",
45
- "inject": [
46
- "@deepseek-ai/dsh-client-locale",
47
- "@deepseek-ai/dsh-client-ui-settings"
48
- ]
45
+ "inject": []
49
46
  }
50
47
  },
51
48
  "peerDependencies": {