@blunking/codexlink 0.1.0 → 0.1.2

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,125 +1,268 @@
1
- param(
2
- [string]$Profile = "default"
3
- )
4
-
5
- $ErrorActionPreference = "Stop"
6
-
7
- function Read-DotEnvFile {
8
- param([string]$Path)
9
- $values = @{}
10
- if (-not (Test-Path $Path)) { return $values }
11
- foreach ($line in (Get-Content -Path $Path)) {
12
- if (-not $line) { continue }
13
- if ($line.Trim().StartsWith("#")) { continue }
14
- $parts = $line -split "=", 2
15
- if ($parts.Count -ne 2) { continue }
16
- $values[$parts[0].Trim()] = $parts[1]
1
+ param(
2
+ [string]$Profile = "default",
3
+ [switch]$Json
4
+ )
5
+
6
+ $ErrorActionPreference = "Stop"
7
+
8
+ function Read-DotEnvFile {
9
+ param([string]$Path)
10
+ $values = @{}
11
+ if (-not (Test-Path $Path)) { return $values }
12
+ foreach ($line in (Get-Content -Path $Path)) {
13
+ if (-not $line) { continue }
14
+ if ($line.Trim().StartsWith("#")) { continue }
15
+ $parts = $line -split "=", 2
16
+ if ($parts.Count -ne 2) { continue }
17
+ $values[$parts[0].Trim()] = $parts[1]
18
+ }
19
+ return $values
20
+ }
21
+
22
+ function Add-Check {
23
+ param(
24
+ [System.Collections.Generic.List[object]]$List,
25
+ [string]$Name,
26
+ [string]$Status,
27
+ [string]$Detail
28
+ )
29
+ $List.Add([pscustomobject]@{
30
+ name = $Name
31
+ status = $Status
32
+ detail = $Detail
33
+ }) | Out-Null
34
+ }
35
+
36
+ function Test-TelegramTokenFormat {
37
+ param([string]$Value)
38
+ if (-not $Value) { return $false }
39
+ return $Value -match '^\d{6,}:[A-Za-z0-9_-]{20,}$'
40
+ }
41
+
42
+ function Test-AllowedChatIdsFormat {
43
+ param([string]$Value)
44
+ if (-not $Value) { return $false }
45
+ $parts = @($Value -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ })
46
+ if ($parts.Count -eq 0) { return $false }
47
+ foreach ($part in $parts) {
48
+ if ($part -notmatch '^-?\d+$') {
49
+ return $false
50
+ }
51
+ }
52
+ return $true
53
+ }
54
+
55
+ function Get-OverallStatus {
56
+ param([System.Collections.Generic.List[object]]$Checks)
57
+ if (@($Checks | Where-Object { $_.status -eq "fail" }).Count -gt 0) {
58
+ return "fail"
59
+ }
60
+ if (@($Checks | Where-Object { $_.status -eq "warn" }).Count -gt 0) {
61
+ return "warn"
62
+ }
63
+ return "ok"
64
+ }
65
+
66
+ function Write-DoctorReport {
67
+ param(
68
+ [object]$Result,
69
+ [string]$TokenSource,
70
+ [string]$AllowedChatSource
71
+ )
72
+
73
+ $emoji = switch ($Result.overall) {
74
+ "ok" { "[OK]" }
75
+ "warn" { "[WARN]" }
76
+ default { "[FAIL]" }
77
+ }
78
+
79
+ Write-Host ""
80
+ Write-Host "CodexLink Telegram Doctor $emoji" -ForegroundColor Cyan
81
+ Write-Host "Profil: $($Result.profile)"
82
+ Write-Host "State-Ordner: $($Result.state_dir)"
83
+ Write-Host ""
84
+
85
+ foreach ($check in $Result.checks) {
86
+ $prefix = switch ($check.status) {
87
+ "ok" { "[OK]" }
88
+ "warn" { "[WARN]" }
89
+ default { "[FAIL]" }
90
+ }
91
+ $color = switch ($check.status) {
92
+ "ok" { "Green" }
93
+ "warn" { "Yellow" }
94
+ default { "Red" }
95
+ }
96
+ Write-Host "$prefix $($check.name): $($check.detail)" -ForegroundColor $color
97
+ }
98
+
99
+ Write-Host ""
100
+ if ($TokenSource) {
101
+ Write-Host "Bot-Token gefunden aus: $TokenSource" -ForegroundColor DarkGray
102
+ }
103
+ if ($AllowedChatSource) {
104
+ Write-Host "Erlaubte Chat-ID(s) gefunden aus: $AllowedChatSource" -ForegroundColor DarkGray
105
+ }
106
+
107
+ $failed = @($Result.checks | Where-Object { $_.status -eq "fail" })
108
+ if ($failed.Count -gt 0) {
109
+ Write-Host ""
110
+ Write-Host "Was jetzt fehlt:" -ForegroundColor Yellow
111
+ foreach ($item in $failed) {
112
+ switch ($item.name) {
113
+ "bot_token" { Write-Host " - Telegram Bot Token fehlt. Starte: blun-codex --profile $($Result.profile) telegram-setup" }
114
+ "allowed_chat_ids" { Write-Host " - Erlaubte Chat-ID(s) fehlen. Starte: blun-codex --profile $($Result.profile) telegram-setup" }
115
+ "state_dir" { Write-Host " - Der lokale Telegram-State-Ordner fehlt noch. Ein Setup-Lauf legt ihn automatisch an." }
116
+ "profile_file" { Write-Host " - Das angegebene Profil existiert nicht." }
117
+ "node" { Write-Host " - Node.js fehlt in PATH." }
118
+ "codex" { Write-Host " - Der lokale codex-Befehl fehlt in PATH." }
119
+ "telegram_plugin_root" { Write-Host " - Der Telegram-Plugin-Ordner konnte nicht gefunden werden." }
120
+ default { Write-Host " - $($item.detail)" }
121
+ }
122
+ }
123
+ }
124
+
125
+ if ($Result.overall -eq "ok") {
126
+ Write-Host ""
127
+ Write-Host "Telegram ist sauber eingerichtet." -ForegroundColor Green
128
+ Write-Host "Starten: blun-codex --profile $($Result.profile) telegram-plugin"
129
+ } elseif ($Result.overall -eq "warn") {
130
+ Write-Host ""
131
+ Write-Host "Die Grundkonfiguration steht, aber es gibt noch Laufzeit-Hinweise." -ForegroundColor Yellow
132
+ Write-Host "Das ist oft normal, wenn Telegram noch nicht aktiv gestartet wurde oder noch keine Nachricht durchlief."
17
133
  }
18
- return $values
19
134
  }
20
135
 
21
- function Add-Check {
136
+ function Get-ProfilePath {
22
137
  param(
23
- [System.Collections.Generic.List[object]]$List,
24
- [string]$Name,
25
- [string]$Status,
26
- [string]$Detail
138
+ [string]$RuntimeRoot,
139
+ [string]$ProfileName
27
140
  )
28
- $List.Add([pscustomobject]@{
29
- name = $Name
30
- status = $Status
31
- detail = $Detail
32
- }) | Out-Null
33
- }
34
141
 
35
- $runtimeRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
36
- $profilePath = Join-Path $runtimeRoot ("profiles\" + $Profile.ToLower() + ".json")
37
- $checks = New-Object 'System.Collections.Generic.List[object]'
38
-
39
- if (Test-Path $profilePath) {
40
- Add-Check -List $checks -Name "profile_file" -Status "ok" -Detail $profilePath
41
- } else {
42
- Add-Check -List $checks -Name "profile_file" -Status "fail" -Detail ("Missing profile: " + $profilePath)
43
- }
44
-
45
- $statusRaw = & powershell -ExecutionPolicy Bypass -File (Join-Path $runtimeRoot "telegram-status.ps1") -Profile $Profile
46
- $status = $statusRaw | ConvertFrom-Json
47
-
48
- $nodeCommand = Get-Command node -ErrorAction SilentlyContinue
49
- $codexCommand = Get-Command codex -ErrorAction SilentlyContinue
50
- Add-Check -List $checks -Name "node" -Status $(if ($nodeCommand) { "ok" } else { "fail" }) -Detail $(if ($nodeCommand) { $nodeCommand.Source } else { "node not found in PATH" })
51
- Add-Check -List $checks -Name "codex" -Status $(if ($codexCommand) { "ok" } else { "fail" }) -Detail $(if ($codexCommand) { $codexCommand.Source } else { "codex not found in PATH" })
52
-
53
- if ($status.plugin_root) {
54
- Add-Check -List $checks -Name "telegram_plugin_root" -Status "ok" -Detail $status.plugin_root
55
- } else {
56
- Add-Check -List $checks -Name "telegram_plugin_root" -Status "fail" -Detail "Telegram plugin root could not be resolved."
57
- }
142
+ $normalized = [string]$ProfileName
143
+ if (-not $normalized) { $normalized = "" }
144
+ $normalized = $normalized.ToLower()
145
+ $candidates = @()
146
+ if ($env:BLUN_CODEX_PROFILE_ROOT) {
147
+ $candidates += (Join-Path $env:BLUN_CODEX_PROFILE_ROOT ($normalized + ".json"))
148
+ }
149
+ $candidates += (Join-Path $env:USERPROFILE (".codex\\profiles\\codexlink\\" + $normalized + ".json"))
150
+ $candidates += (Join-Path $RuntimeRoot ("profiles\\" + $normalized + ".json"))
58
151
 
59
- if ($status.state_dir -and (Test-Path $status.state_dir)) {
60
- Add-Check -List $checks -Name "state_dir" -Status "ok" -Detail $status.state_dir
61
- } else {
62
- Add-Check -List $checks -Name "state_dir" -Status "fail" -Detail ("Missing state dir: " + $status.state_dir)
63
- }
152
+ foreach ($candidate in $candidates) {
153
+ if ($candidate -and (Test-Path $candidate)) {
154
+ return $candidate
155
+ }
156
+ }
64
157
 
65
- $activeEnv = Read-DotEnvFile -Path (Join-Path $status.state_dir ".env")
66
- $legacyEnv = Read-DotEnvFile -Path (Join-Path $env:USERPROFILE ".codex\channels\codexlink-telegram\.env")
67
- $tokenSource = if ($activeEnv["BLUN_TELEGRAM_BOT_TOKEN"]) {
68
- "active_state_env"
69
- } elseif ($legacyEnv["BLUN_TELEGRAM_BOT_TOKEN"]) {
70
- "legacy_env_fallback"
71
- } else {
72
- ""
158
+ return $candidates[-1]
73
159
  }
74
- Add-Check -List $checks -Name "bot_token" -Status $(if ($tokenSource) { "ok" } else { "fail" }) -Detail $(if ($tokenSource) { $tokenSource } else { "No BLUN_TELEGRAM_BOT_TOKEN found in active or legacy env files." })
75
160
 
161
+ $runtimeRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
162
+ $profilePath = Get-ProfilePath -RuntimeRoot $runtimeRoot -ProfileName $Profile
163
+ $checks = New-Object 'System.Collections.Generic.List[object]'
164
+
165
+ if (Test-Path $profilePath) {
166
+ Add-Check -List $checks -Name "profile_file" -Status "ok" -Detail $profilePath
167
+ } else {
168
+ Add-Check -List $checks -Name "profile_file" -Status "fail" -Detail ("Missing profile: " + $profilePath)
169
+ }
170
+
171
+ $statusRaw = & powershell -ExecutionPolicy Bypass -File (Join-Path $runtimeRoot "telegram-status.ps1") -Profile $Profile
172
+ $status = $statusRaw | ConvertFrom-Json
173
+
174
+ $nodeCommand = Get-Command node -ErrorAction SilentlyContinue
175
+ $codexCommand = Get-Command codex -ErrorAction SilentlyContinue
176
+ Add-Check -List $checks -Name "node" -Status $(if ($nodeCommand) { "ok" } else { "fail" }) -Detail $(if ($nodeCommand) { $nodeCommand.Source } else { "node not found in PATH" })
177
+ Add-Check -List $checks -Name "codex" -Status $(if ($codexCommand) { "ok" } else { "fail" }) -Detail $(if ($codexCommand) { $codexCommand.Source } else { "codex not found in PATH" })
178
+
179
+ if ($status.plugin_root) {
180
+ Add-Check -List $checks -Name "telegram_plugin_root" -Status "ok" -Detail $status.plugin_root
181
+ } else {
182
+ Add-Check -List $checks -Name "telegram_plugin_root" -Status "fail" -Detail "Telegram plugin root could not be resolved."
183
+ }
184
+
185
+ if ($status.state_dir -and (Test-Path $status.state_dir)) {
186
+ Add-Check -List $checks -Name "state_dir" -Status "ok" -Detail $status.state_dir
187
+ } else {
188
+ Add-Check -List $checks -Name "state_dir" -Status "fail" -Detail ("Missing state dir: " + $status.state_dir)
189
+ }
190
+
191
+ $activeEnvPath = Join-Path $status.state_dir ".env"
192
+ $activeEnv = Read-DotEnvFile -Path $activeEnvPath
193
+ $legacyEnvPath = Join-Path $env:USERPROFILE ".codex\channels\codexlink-telegram\.env"
194
+ $legacyEnv = Read-DotEnvFile -Path $legacyEnvPath
195
+
196
+ $tokenValue = ""
197
+ $tokenSource = ""
198
+ if (Test-TelegramTokenFormat -Value $activeEnv["BLUN_TELEGRAM_BOT_TOKEN"]) {
199
+ $tokenValue = [string]$activeEnv["BLUN_TELEGRAM_BOT_TOKEN"]
200
+ $tokenSource = "state env"
201
+ } elseif (Test-TelegramTokenFormat -Value $legacyEnv["BLUN_TELEGRAM_BOT_TOKEN"]) {
202
+ $tokenValue = [string]$legacyEnv["BLUN_TELEGRAM_BOT_TOKEN"]
203
+ $tokenSource = "legacy env fallback"
204
+ }
205
+ Add-Check -List $checks -Name "bot_token" -Status $(if ($tokenValue) { "ok" } else { "fail" }) -Detail $(if ($tokenValue) { $tokenSource } else { "No valid BLUN_TELEGRAM_BOT_TOKEN found." })
206
+
207
+ $allowedChatIds = ""
208
+ $allowedChatSource = ""
209
+ if (Test-AllowedChatIdsFormat -Value $activeEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"]) {
210
+ $allowedChatIds = [string]$activeEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"]
211
+ $allowedChatSource = "state env"
212
+ } elseif (Test-AllowedChatIdsFormat -Value $legacyEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"]) {
213
+ $allowedChatIds = [string]$legacyEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"]
214
+ $allowedChatSource = "legacy env fallback"
215
+ }
216
+ Add-Check -List $checks -Name "allowed_chat_ids" -Status $(if ($allowedChatIds) { "ok" } else { "fail" }) -Detail $(if ($allowedChatIds) { $allowedChatIds } else { "No valid BLUN_TELEGRAM_ALLOWED_CHAT_ID found." })
217
+
76
218
  Add-Check -List $checks -Name "app_server_ws" -Status $(if ($status.active_ws) { "ok" } else { "warn" }) -Detail $(if ($status.active_ws) { $status.active_ws } else { "No active websocket recorded." })
219
+ Add-Check -List $checks -Name "dispatch_mode" -Status $(if ($status.dispatch_mode -eq "deferred") { "ok" } else { "warn" }) -Detail ("mode=" + [string]$status.dispatch_mode + " cooldown_ms=" + [string]$status.idle_cooldown_ms)
77
220
  Add-Check -List $checks -Name "bound_thread" -Status $(if ($status.active_thread_id) { "ok" } else { "warn" }) -Detail $(if ($status.active_thread_id) { $status.active_thread_id } else { "No active thread bound yet." })
78
- Add-Check -List $checks -Name "poller" -Status $(if ($status.poller_alive) { "ok" } else { "warn" }) -Detail ("pid=" + [string]$status.poller_pid + " alive=" + [string]$status.poller_alive)
79
- Add-Check -List $checks -Name "dispatcher" -Status $(if ($status.dispatcher_alive) { "ok" } else { "warn" }) -Detail ("pid=" + [string]$status.dispatcher_pid + " alive=" + [string]$status.dispatcher_alive)
80
- Add-Check -List $checks -Name "responder" -Status $(if ($status.responder_alive) { "ok" } else { "warn" }) -Detail ("pid=" + [string]$status.responder_pid + " alive=" + [string]$status.responder_alive)
81
-
82
- if ($status.last_inbound) {
83
- $lastInboundSummary = [string]::Format(
84
- "chat={0} message={1} type={2} thread={3}",
85
- $status.last_inbound.chatId,
86
- $status.last_inbound.messageId,
87
- $status.last_inbound.chatType,
88
- $(if ($status.last_inbound.telegramThreadId) { $status.last_inbound.telegramThreadId } else { "-" })
89
- )
90
- Add-Check -List $checks -Name "last_inbound" -Status "ok" -Detail $lastInboundSummary
91
- } else {
92
- Add-Check -List $checks -Name "last_inbound" -Status "warn" -Detail "No inbound Telegram message recorded yet."
93
- }
94
-
95
- if ($status.last_outbound) {
96
- $lastOutboundSummary = [string]::Format(
97
- "chat={0} message={1} reply_to={2} thread={3}",
98
- $status.last_outbound.chatId,
99
- $status.last_outbound.messageId,
100
- $(if ($status.last_outbound.replyToMessageId) { $status.last_outbound.replyToMessageId } else { "-" }),
101
- $(if ($status.last_outbound.telegramThreadId) { $status.last_outbound.telegramThreadId } else { "-" })
102
- )
103
- Add-Check -List $checks -Name "last_outbound" -Status "ok" -Detail $lastOutboundSummary
104
- } else {
105
- Add-Check -List $checks -Name "last_outbound" -Status "warn" -Detail "No outbound Telegram message recorded yet."
106
- }
107
-
108
- Add-Check -List $checks -Name "queue" -Status $(if (([int]$status.queue_depth -eq 0) -and ([int]$status.pending_reply_depth -eq 0)) { "ok" } else { "warn" }) -Detail ("queued=" + $status.queue_depth + " submitted=" + $status.submitted_depth + " pending_replies=" + $status.pending_reply_depth)
109
-
110
- $overall = "ok"
111
- if (@($checks | Where-Object { $_.status -eq "fail" }).Count -gt 0) {
112
- $overall = "fail"
113
- } elseif (@($checks | Where-Object { $_.status -eq "warn" }).Count -gt 0) {
114
- $overall = "warn"
115
- }
116
-
117
- [ordered]@{
118
- profile = $status.profile
119
- overall = $overall
120
- runtime_root = $runtimeRoot
121
- state_dir = $status.state_dir
122
- plugin_root = $status.plugin_root
123
- checks = $checks
124
- status = $status
125
- } | ConvertTo-Json -Depth 8
221
+ Add-Check -List $checks -Name "poller" -Status $(if ($status.poller_alive) { "ok" } else { "warn" }) -Detail ("pid=" + [string]$status.poller_pid + " alive=" + [string]$status.poller_alive)
222
+ Add-Check -List $checks -Name "dispatcher" -Status $(if ($status.dispatcher_alive) { "ok" } else { "warn" }) -Detail ("pid=" + [string]$status.dispatcher_pid + " alive=" + [string]$status.dispatcher_alive)
223
+ Add-Check -List $checks -Name "responder" -Status $(if ($status.responder_alive) { "ok" } else { "warn" }) -Detail ("pid=" + [string]$status.responder_pid + " alive=" + [string]$status.responder_alive)
224
+
225
+ if ($status.last_inbound) {
226
+ $lastInboundSummary = [string]::Format(
227
+ "chat={0} message={1} type={2} thread={3}",
228
+ $status.last_inbound.chatId,
229
+ $status.last_inbound.messageId,
230
+ $status.last_inbound.chatType,
231
+ $(if ($status.last_inbound.telegramThreadId) { $status.last_inbound.telegramThreadId } else { "-" })
232
+ )
233
+ Add-Check -List $checks -Name "last_inbound" -Status "ok" -Detail $lastInboundSummary
234
+ } else {
235
+ Add-Check -List $checks -Name "last_inbound" -Status "warn" -Detail "No inbound Telegram message recorded yet."
236
+ }
237
+
238
+ if ($status.last_outbound) {
239
+ $lastOutboundSummary = [string]::Format(
240
+ "chat={0} message={1} reply_to={2} thread={3}",
241
+ $status.last_outbound.chatId,
242
+ $status.last_outbound.messageId,
243
+ $(if ($status.last_outbound.replyToMessageId) { $status.last_outbound.replyToMessageId } else { "-" }),
244
+ $(if ($status.last_outbound.telegramThreadId) { $status.last_outbound.telegramThreadId } else { "-" })
245
+ )
246
+ Add-Check -List $checks -Name "last_outbound" -Status "ok" -Detail $lastOutboundSummary
247
+ } else {
248
+ Add-Check -List $checks -Name "last_outbound" -Status "warn" -Detail "No outbound Telegram message recorded yet."
249
+ }
250
+
251
+ Add-Check -List $checks -Name "queue" -Status $(if (([int]$status.queue_depth -eq 0) -and ([int]$status.pending_reply_depth -eq 0)) { "ok" } else { "warn" }) -Detail ("queued=" + $status.queue_depth + " ambient=" + $status.ambient_queue_depth + " submitted=" + $status.submitted_depth + " pending_replies=" + $status.pending_reply_depth)
252
+
253
+ $result = [ordered]@{
254
+ profile = $status.profile
255
+ overall = Get-OverallStatus -Checks $checks
256
+ runtime_root = $runtimeRoot
257
+ state_dir = $status.state_dir
258
+ plugin_root = $status.plugin_root
259
+ checks = $checks
260
+ status = $status
261
+ }
262
+
263
+ if ($Json) {
264
+ $result | ConvertTo-Json -Depth 8
265
+ exit 0
266
+ }
267
+
268
+ Write-DoctorReport -Result $result -TokenSource $tokenSource -AllowedChatSource $allowedChatSource
@@ -1,9 +1,11 @@
1
1
  BLUN_TELEGRAM_AGENT_NAME=default
2
2
  BLUN_TELEGRAM_BOT_TOKEN=123456789:replace_me
3
- BLUN_TELEGRAM_ALLOWED_CHAT_ID=1605241602
3
+ BLUN_TELEGRAM_ALLOWED_CHAT_ID=1605241602,-1003927574737
4
4
  BLUN_TELEGRAM_CODEX_BIN=codex
5
5
  BLUN_TELEGRAM_STATE_DIR=
6
6
  BLUN_TELEGRAM_THREAD_ID=
7
7
  BLUN_TELEGRAM_RESUME_TIMEOUT_MS=15000
8
+ BLUN_TELEGRAM_IDLE_COOLDOWN_MS=15000
9
+ BLUN_TELEGRAM_DISPATCH_MODE=deferred
8
10
  BLUN_TELEGRAM_POLL_INTERVAL_MS=5000
9
11
  BLUN_TELEGRAM_INJECT_INTERVAL_MS=15000
@@ -10,8 +10,10 @@ It is intentionally **not** an autonomous answer bot.
10
10
  - stores inbound and outbound history under a local state directory
11
11
  - keeps private chats and group threads separated
12
12
  - binds a live thread id
13
- - injects the next queued Telegram message into that exact live thread
13
+ - injects the next queued Telegram message into that exact live thread only after the session is idle
14
14
  - sends explicit manual replies from the visible operator session
15
+ - keeps ambient group noise queued unless it is relevant to that operator
16
+ - lets escalation-style messages bypass the normal idle queue
15
17
 
16
18
  ## What it does not do
17
19
 
@@ -44,10 +46,12 @@ Copy `.env.example` to `.env` in the state directory or export env vars:
44
46
 
45
47
  - `BLUN_TELEGRAM_AGENT_NAME`
46
48
  - `BLUN_TELEGRAM_BOT_TOKEN`
47
- - `BLUN_TELEGRAM_ALLOWED_CHAT_ID`
49
+ - `BLUN_TELEGRAM_ALLOWED_CHAT_ID` (`chatId` or comma-separated list like `1605241602,-1003927574737`)
48
50
  - `BLUN_TELEGRAM_CODEX_BIN`
49
51
  - `BLUN_TELEGRAM_THREAD_ID`
50
52
  - `BLUN_TELEGRAM_RESUME_TIMEOUT_MS`
53
+ - `BLUN_TELEGRAM_IDLE_COOLDOWN_MS`
54
+ - `BLUN_TELEGRAM_DISPATCH_MODE` (`deferred` by default, `legacy` to restore eager dispatch)
51
55
 
52
56
  ## Tools
53
57
 
@@ -63,6 +67,6 @@ Copy `.env.example` to `.env` in the state directory or export env vars:
63
67
  ## Runtime split
64
68
 
65
69
  - `poller.js` only fetches Telegram updates into the queue
66
- - `dispatcher.js` only retries queue delivery into the bound live thread
70
+ - `dispatcher.js` only retries queue delivery into the bound live thread after the current run is quiet
67
71
  - `responder.js` only relays finished answers back out
68
72
  - none of them are allowed to invent an answer on their own
@@ -19,8 +19,8 @@ process.on("SIGTERM", () => {
19
19
  async function main() {
20
20
  while (!stopping) {
21
21
  try {
22
- const result = await injectNext("");
23
- if (result.status !== "empty") {
22
+ const result = await injectNext("", { auto: true });
23
+ if (!["empty", "deferred"].includes(result.status)) {
24
24
  console.log(JSON.stringify({ ts: new Date().toISOString(), kind: "inject", result }));
25
25
  }
26
26
  } catch (error) {