@blunking/codexlink 0.1.2 → 0.1.9

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/README.md CHANGED
@@ -11,15 +11,16 @@
11
11
 
12
12
  CodexLink is the BLUN launcher for one visible CLI session with optional Telegram delivery.
13
13
 
14
- It keeps transport and queueing around the operator, without spinning up a hidden second session.
15
-
16
- Telegram delivery is serial by default:
17
-
18
- - inbound messages land in queue first
19
- - active work is not interrupted immediately
20
- - direct messages wait until the visible session is quiet
21
- - ambient group noise stays queued until it is relevant or manually drained
22
- - escalation-style messages can still jump the line
14
+ It keeps transport and queueing around the operator, without spinning up a hidden second session.
15
+
16
+ Telegram delivery is serial by default:
17
+
18
+ - inbound messages land in queue first
19
+ - active work is not interrupted immediately
20
+ - direct messages wait until the visible session is quiet
21
+ - ambient group noise stays queued until it is relevant or manually drained
22
+ - escalation-style messages can still jump the line
23
+ - stale pending replies time out automatically, so the queue cannot block forever
23
24
 
24
25
  ## Install
25
26
 
@@ -108,7 +109,18 @@ Status:
108
109
  blun-codex telegram-status
109
110
  ```
110
111
 
111
- Doctor:
112
+ Wenn waehrend einer laufenden Arbeit Telegram-Nachrichten gepuffert werden, bleibt die sichtbare CLI-Eingabe unberuehrt. Pending-Nachrichten bleiben im Fenstertitel/Status sichtbar, bis die Antwort raus ist oder sie wirklich ablaufen. Den Queue-Stand kannst du jederzeit mit `blun-codex telegram-status` pruefen.
113
+
114
+ Der automatische Progress-Hinweis ist bewusst defensiv: standardmaessig sendet Telegram nur finale Antworten plus bei laengeren echten Arbeitslaeufen einen neutralen Status. Interne Commentary-Texte werden nicht als zweite fachliche Antwort gespiegelt. Wer das alte Verhalten will, kann `BLUN_TELEGRAM_PROGRESS_RELAY=commentary` setzen; mit `off` werden Progress-Hinweise ganz deaktiviert.
115
+
116
+ Wenn mehrere Agents denselben Gruppenchat nutzen, kann ein Agent andere Agent-Namen als Fremdroute markieren. Dann werden Owner-Nachrichten wie `Frida mach weiter` nicht in Ottos Session gezogen:
117
+
118
+ ```text
119
+ BLUN_TELEGRAM_MENTION_NAMES=otto
120
+ BLUN_TELEGRAM_OTHER_AGENT_NAMES=frida,angel,dieter,alfred
121
+ ```
122
+
123
+ Doctor:
112
124
 
113
125
  ```powershell
114
126
  blun-codex telegram-doctor
@@ -160,39 +172,39 @@ Why this matters:
160
172
  - starting a second operator on `default` will replace the first `default` runtime
161
173
  - a private profile gives that operator a separate runtime slot, state directory, and Mnemo binding
162
174
 
163
- For internal/private profiles:
164
-
165
- - keep the profile local on the machine
166
- - give it its own `agent_name`
167
- - give it its own Telegram state directory
168
- - do not ship internal agent profiles in the public package
169
-
170
- Local private profiles are loaded from:
171
-
172
- ```text
173
- %USERPROFILE%\.codex\profiles\codexlink\<name>.json
174
- ```
175
-
176
- Example:
177
-
178
- ```powershell
179
- blun-codex --profile frida telegram-plugin
180
- ```
181
-
182
- looks for:
183
-
184
- ```text
185
- %USERPROFILE%\.codex\profiles\codexlink\frida.json
186
- ```
175
+ For internal/private profiles:
176
+
177
+ - keep the profile local on the machine
178
+ - give it its own `agent_name`
179
+ - give it its own Telegram state directory
180
+ - do not ship internal agent profiles in the public package
181
+
182
+ Local private profiles are loaded from:
183
+
184
+ ```text
185
+ %USERPROFILE%\.codex\profiles\codexlink\<name>.json
186
+ ```
187
+
188
+ Example:
189
+
190
+ ```powershell
191
+ blun-codex --profile frida telegram-plugin
192
+ ```
193
+
194
+ looks for:
195
+
196
+ ```text
197
+ %USERPROFILE%\.codex\profiles\codexlink\frida.json
198
+ ```
187
199
 
188
200
  ## What it does
189
201
 
190
202
  - starts one consistent local CLI runtime
191
203
  - writes a launch record into `.codex/runtimes/default/`
192
204
  - keeps Telegram queue state under `.codex/channels/telegram-default/`
193
- - attaches Telegram delivery to the same visible session
194
- - defers automatic Telegram delivery until the foreground session is idle
195
- - keeps poller, dispatcher, and reply relay separate from the foreground operator
205
+ - attaches Telegram delivery to the same visible session
206
+ - defers automatic Telegram delivery until the foreground session is idle
207
+ - keeps poller, dispatcher, and reply relay separate from the foreground operator
196
208
 
197
209
  ## What it does not do
198
210
 
@@ -232,10 +244,16 @@ The bundled plugin lives under `telegram-plugin/` and contains:
232
244
  `blun-codex telegram-plugin` now behaves like a guided setup for normal users:
233
245
 
234
246
  1. check whether Telegram is already configured
235
- 2. ask for missing Bot Token or allowed Chat ID(s)
247
+ 2. ask only for a missing Bot Token
236
248
  3. save everything automatically into the local Telegram state folder
237
249
  4. continue into Telegram mode
238
250
 
251
+ Allowed Chat ID(s) are optional. If you leave them unset, the bot can currently accept any chat it can see. You can tighten that later with:
252
+
253
+ ```powershell
254
+ blun-codex telegram-setup
255
+ ```
256
+
239
257
  If something is missing later, `blun-codex telegram-doctor` tells you exactly what is missing and what to run next.
240
258
 
241
259
  ## Notes
package/package.json CHANGED
@@ -1,38 +1,40 @@
1
- {
2
- "name": "@blunking/codexlink",
3
- "version": "0.1.2",
4
- "description": "BLUN CLI launcher with Telegram channel support for one visible session.",
5
- "license": "MIT",
6
- "private": false,
7
- "bin": {
8
- "blun-codex": "./bin/blun-codex.js",
9
- "codexlink": "./bin/blun-codex.js"
10
- },
11
- "files": [
12
- "bin/",
13
- "profiles/default.json",
14
- "telegram-plugin/",
15
- "README.md",
16
- "LICENSE",
17
- "blun-codex.cmd",
18
- "blun-codex.ps1",
19
- "start-codex-agent.ps1",
20
- "start-codex.cmd",
21
- "telegram-status.ps1",
22
- "telegram-doctor.ps1",
23
- "telegram-setup.ps1"
24
- ],
25
- "keywords": [
26
- "blun",
27
- "codexlink",
28
- "telegram",
29
- "runtime",
30
- "cli"
31
- ],
32
- "dependencies": {
33
- "@modelcontextprotocol/sdk": "^1.0.0"
34
- },
35
- "engines": {
36
- "node": ">=20"
37
- }
38
- }
1
+ {
2
+ "name": "@blunking/codexlink",
3
+ "version": "0.1.9",
4
+ "description": "BLUN CLI launcher with Telegram channel support for one visible session.",
5
+ "license": "MIT",
6
+ "private": false,
7
+ "bin": {
8
+ "blun-codex": "bin/blun-codex.js",
9
+ "codexlink": "bin/blun-codex.js"
10
+ },
11
+ "files": [
12
+ "bin/",
13
+ "profiles/default.json",
14
+ "telegram-plugin/",
15
+ "README.md",
16
+ "LICENSE",
17
+ "blun-codex.cmd",
18
+ "blun-codex.ps1",
19
+ "start-codex-agent.ps1",
20
+ "start-codex.cmd",
21
+ "telegram-title-embed.ps1",
22
+ "telegram-status.ps1",
23
+ "telegram-doctor.ps1",
24
+ "telegram-setup.ps1",
25
+ "telegram-title-watcher.ps1"
26
+ ],
27
+ "keywords": [
28
+ "blun",
29
+ "codexlink",
30
+ "telegram",
31
+ "runtime",
32
+ "cli"
33
+ ],
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.0.0"
36
+ },
37
+ "engines": {
38
+ "node": ">=20"
39
+ }
40
+ }
@@ -21,42 +21,42 @@ function Get-JsonFile {
21
21
  return Get-Content -Raw -Path $Path | ConvertFrom-Json
22
22
  }
23
23
 
24
- function Try-GetJsonFile {
25
- param([string]$Path)
26
- if (-not (Test-Path $Path)) {
27
- return $null
24
+ function Try-GetJsonFile {
25
+ param([string]$Path)
26
+ if (-not (Test-Path $Path)) {
27
+ return $null
28
28
  }
29
29
  try {
30
30
  return Get-Content -Raw -Path $Path | ConvertFrom-Json
31
31
  } catch {
32
32
  return $null
33
- }
34
- }
35
-
36
- function Get-ProfilePath {
37
- param(
38
- [string]$RuntimeRoot,
39
- [string]$ProfileName
40
- )
41
-
42
- $normalized = [string]$ProfileName
43
- if (-not $normalized) { $normalized = "" }
44
- $normalized = $normalized.ToLower()
45
- $candidates = @()
46
- if ($env:BLUN_CODEX_PROFILE_ROOT) {
47
- $candidates += (Join-Path $env:BLUN_CODEX_PROFILE_ROOT ($normalized + ".json"))
48
- }
49
- $candidates += (Join-Path $env:USERPROFILE (".codex\\profiles\\codexlink\\" + $normalized + ".json"))
50
- $candidates += (Join-Path $RuntimeRoot ("profiles\\" + $normalized + ".json"))
51
-
52
- foreach ($candidate in $candidates) {
53
- if ($candidate -and (Test-Path $candidate)) {
54
- return $candidate
55
- }
56
- }
57
-
58
- return $candidates[-1]
59
- }
33
+ }
34
+ }
35
+
36
+ function Get-ProfilePath {
37
+ param(
38
+ [string]$RuntimeRoot,
39
+ [string]$ProfileName
40
+ )
41
+
42
+ $normalized = [string]$ProfileName
43
+ if (-not $normalized) { $normalized = "" }
44
+ $normalized = $normalized.ToLower()
45
+ $candidates = @()
46
+ if ($env:BLUN_CODEX_PROFILE_ROOT) {
47
+ $candidates += (Join-Path $env:BLUN_CODEX_PROFILE_ROOT ($normalized + ".json"))
48
+ }
49
+ $candidates += (Join-Path $env:USERPROFILE (".codex\\profiles\\codexlink\\" + $normalized + ".json"))
50
+ $candidates += (Join-Path $RuntimeRoot ("profiles\\" + $normalized + ".json"))
51
+
52
+ foreach ($candidate in $candidates) {
53
+ if ($candidate -and (Test-Path $candidate)) {
54
+ return $candidate
55
+ }
56
+ }
57
+
58
+ return $candidates[-1]
59
+ }
60
60
 
61
61
  function Ensure-Dir {
62
62
  param([string]$Path)
@@ -113,7 +113,7 @@ function Get-FreeTcpPort {
113
113
  }
114
114
  }
115
115
 
116
- function Stop-ProcessTree {
116
+ function Stop-ProcessTree {
117
117
  param([int[]]$RootIds)
118
118
 
119
119
  $all = @(Get-CimInstance Win32_Process)
@@ -144,9 +144,9 @@ function Stop-ProcessTree {
144
144
  } catch {
145
145
  }
146
146
  }
147
- }
148
-
149
- function Wait-TcpPort {
147
+ }
148
+
149
+ function Wait-TcpPort {
150
150
  param(
151
151
  [string]$HostName,
152
152
  [int]$Port,
@@ -301,8 +301,8 @@ function Quote-PowerShellLiteral {
301
301
  }
302
302
 
303
303
  $runtimeRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
304
- $profilePath = Get-ProfilePath -RuntimeRoot $runtimeRoot -ProfileName $Agent
305
- $profile = Get-JsonFile -Path $profilePath
304
+ $profilePath = Get-ProfilePath -RuntimeRoot $runtimeRoot -ProfileName $Agent
305
+ $profile = Get-JsonFile -Path $profilePath
306
306
 
307
307
  $resolvedWorkspace = if ($Workspace) { $Workspace } elseif ($profile.workspace) { $profile.workspace } else { (Get-Location).Path }
308
308
  $resolvedWorkspace = Resolve-ConfiguredPath -Value $resolvedWorkspace
@@ -527,13 +527,24 @@ if ($useRemoteAppServer) {
527
527
  if ($telegramAllowedChatId) {
528
528
  $stateEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"] = $telegramAllowedChatId
529
529
  }
530
- $stateEnv["BLUN_TELEGRAM_PLUGIN_MODE"] = $TelegramMode
531
- $stateEnv["BLUN_TELEGRAM_APP_SERVER_WS_URL"] = $telegramAppServerWsUrl
532
- $stateEnv["BLUN_TELEGRAM_THREAD_ID"] = ""
533
- Write-DotEnvFile -Path $envFilePath -Values $stateEnv
534
- Write-DebugStage -Path $debugLogPath -Message ("ENV_WRITTEN ws_url=" + $telegramAppServerWsUrl + " env_file=" + $envFilePath)
535
-
536
- Set-EnvVar "BLUN_TELEGRAM_APP_SERVER_WS_URL" $telegramAppServerWsUrl
530
+ $stateEnv["BLUN_TELEGRAM_PLUGIN_MODE"] = $TelegramMode
531
+ $stateEnv["BLUN_TELEGRAM_APP_SERVER_WS_URL"] = $telegramAppServerWsUrl
532
+ $stateEnv["BLUN_TELEGRAM_THREAD_ID"] = ""
533
+ Write-DotEnvFile -Path $envFilePath -Values $stateEnv
534
+ Write-DebugStage -Path $debugLogPath -Message ("ENV_WRITTEN ws_url=" + $telegramAppServerWsUrl + " env_file=" + $envFilePath)
535
+ $telegramStateFile = Join-Path $telegramStateDir "state.json"
536
+ $telegramState = Try-GetJsonFile -Path $telegramStateFile
537
+ if ($null -ne $telegramState) {
538
+ if ($telegramState.PSObject.Properties.Name.Contains("currentThreadId")) {
539
+ $telegramState.currentThreadId = ""
540
+ } else {
541
+ $telegramState | Add-Member -NotePropertyName "currentThreadId" -NotePropertyValue ""
542
+ }
543
+ Write-TextFileWithRetry -Path $telegramStateFile -Content ($telegramState | ConvertTo-Json -Depth 10)
544
+ Write-DebugStage -Path $debugLogPath -Message "STATE_THREAD_CLEARED"
545
+ }
546
+
547
+ Set-EnvVar "BLUN_TELEGRAM_APP_SERVER_WS_URL" $telegramAppServerWsUrl
537
548
 
538
549
  $appServerPidFile = Join-Path $agentRuntimeDir "app-server.pid"
539
550
  $appServerInfoFile = Join-Path $agentRuntimeDir "app-server.json"
@@ -545,6 +556,7 @@ if ($useRemoteAppServer) {
545
556
  $oldPids = @()
546
557
  if ($previousRuntime.frontend_host_pid) { $oldPids += [int]$previousRuntime.frontend_host_pid }
547
558
  if ($previousRuntime.app_server_pid) { $oldPids += [int]$previousRuntime.app_server_pid }
559
+ if ($previousRuntime.queue_notifier_pid) { $oldPids += [int]$previousRuntime.queue_notifier_pid }
548
560
  if ($previousRuntime.poller_pid) { $oldPids += [int]$previousRuntime.poller_pid }
549
561
  if ($previousRuntime.dispatcher_pid) { $oldPids += [int]$previousRuntime.dispatcher_pid }
550
562
  if ($previousRuntime.responder_pid) { $oldPids += [int]$previousRuntime.responder_pid }
@@ -666,26 +678,57 @@ if ($PrintOnly) {
666
678
  if ($useRemoteAppServer) {
667
679
  $codexScript = (Get-Command codex).Source
668
680
  $currentRuntimeFile = Join-Path $agentRuntimeDir "current-remote-runtime.json"
669
- $windowTitle = "BLUN Codex Telegram [" + $profile.agent_name + "] " + $telegramAppServerWsUrl
670
- $resumeCommand = "$host.UI.RawUI.WindowTitle = " + (Quote-PowerShellLiteral $windowTitle) + "; & " + (Quote-PowerShellLiteral $codexScript) + " " + (($codexArgs | ForEach-Object { Quote-PowerShellLiteral $_ }) -join " ")
681
+ $windowTitle = "BLUN Codex Telegram [" + $profile.agent_name + "] " + $telegramAppServerWsUrl
682
+ $titleEmbedScript = Join-Path $runtimeRoot "telegram-title-embed.ps1"
683
+ $titleEmbedLog = Join-Path $agentRuntimeDir "title-embed.log"
684
+ $resumeCommand = ""
685
+ if (Test-Path $titleEmbedScript) {
686
+ $resumeCommand += "& " + (Quote-PowerShellLiteral $titleEmbedScript)
687
+ $resumeCommand += " -StateFile " + (Quote-PowerShellLiteral (Join-Path $telegramStateDir "state.json"))
688
+ $resumeCommand += " -BaseTitle " + (Quote-PowerShellLiteral $windowTitle)
689
+ $resumeCommand += " -LogFile " + (Quote-PowerShellLiteral $titleEmbedLog)
690
+ $resumeCommand += "; "
691
+ }
692
+ $resumeCommand += "$host.UI.RawUI.WindowTitle = " + (Quote-PowerShellLiteral $windowTitle) + "; & " + (Quote-PowerShellLiteral $codexScript) + " " + (($codexArgs | ForEach-Object { Quote-PowerShellLiteral $_ }) -join " ")
671
693
  Write-DebugStage -Path $debugLogPath -Message ("FRONTEND_SPAWN command=" + $resumeCommand)
672
694
  $frontendProcess = Start-Process -FilePath "powershell" -WorkingDirectory $resolvedWorkspace -ArgumentList @(
673
695
  "-NoExit",
674
696
  "-Command",
675
697
  $resumeCommand
676
698
  ) -PassThru
677
- $currentRuntime = [ordered]@{
678
- ws_url = $telegramAppServerWsUrl
679
- app_server_pid = $backendProcess.Id
680
- frontend_host_pid = $frontendProcess.Id
681
- profile = $profile.agent_name
699
+ $currentRuntime = [ordered]@{
700
+ ws_url = $telegramAppServerWsUrl
701
+ app_server_pid = $backendProcess.Id
702
+ frontend_host_pid = $frontendProcess.Id
703
+ profile = $profile.agent_name
682
704
  started_at = (Get-Date).ToUniversalTime().ToString("o")
683
- }
684
- Write-TextFileWithRetry -Path $currentRuntimeFile -Content ($currentRuntime | ConvertTo-Json -Depth 4)
685
- Write-DebugStage -Path $debugLogPath -Message "FRONTEND_SPAWNED"
686
- if ($envFilePath -and $bootstrapScript) {
687
- try {
688
- Write-DebugStage -Path $debugLogPath -Message "LOADED_THREAD_WAIT_START"
705
+ }
706
+ Write-TextFileWithRetry -Path $currentRuntimeFile -Content ($currentRuntime | ConvertTo-Json -Depth 4)
707
+ Write-DebugStage -Path $debugLogPath -Message "FRONTEND_SPAWNED"
708
+ $sidecarsStartedEarly = $false
709
+ if ($sidecarManager) {
710
+ try {
711
+ & node $sidecarManager | Out-Null
712
+ if ($LASTEXITCODE -ne 0) {
713
+ Write-DebugStage -Path $debugLogPath -Message "SIDECAR_MANAGER_EARLY_FAILED"
714
+ } else {
715
+ $sidecarsStartedEarly = $true
716
+ Write-DebugStage -Path $debugLogPath -Message "SIDECAR_MANAGER_EARLY_OK"
717
+ $pollerPid = Read-PidFileValue -Path (Join-Path $telegramStateDir "poller.pid")
718
+ $dispatcherPid = Read-PidFileValue -Path (Join-Path $telegramStateDir "dispatcher.pid")
719
+ $responderPid = Read-PidFileValue -Path (Join-Path $telegramStateDir "responder.pid")
720
+ if ($pollerPid -gt 0) { $currentRuntime["poller_pid"] = $pollerPid }
721
+ if ($dispatcherPid -gt 0) { $currentRuntime["dispatcher_pid"] = $dispatcherPid }
722
+ if ($responderPid -gt 0) { $currentRuntime["responder_pid"] = $responderPid }
723
+ Write-TextFileWithRetry -Path $currentRuntimeFile -Content ($currentRuntime | ConvertTo-Json -Depth 6)
724
+ }
725
+ } catch {
726
+ Write-DebugStage -Path $debugLogPath -Message ("SIDECAR_MANAGER_EARLY_ERROR " + $_.Exception.Message)
727
+ }
728
+ }
729
+ if ($envFilePath -and $bootstrapScript) {
730
+ try {
731
+ Write-DebugStage -Path $debugLogPath -Message "LOADED_THREAD_WAIT_START"
689
732
  $loadedIds = @()
690
733
  for ($attempt = 1; $attempt -le 40; $attempt++) {
691
734
  $loaded = Invoke-NodeJsonWithRetry -NodeArgs @($bootstrapScript, "list-loaded", "--ws-url", $telegramAppServerWsUrl) -Attempts 1 -DelayMs 0
@@ -697,17 +740,67 @@ if ($useRemoteAppServer) {
697
740
  if ($attempt -lt 40) {
698
741
  Start-Sleep -Milliseconds 500
699
742
  }
700
- }
701
- if ($loadedIds.Count -gt 0) {
702
- $activeThreadId = [string]$loadedIds[$loadedIds.Count - 1]
703
- Write-DebugStage -Path $debugLogPath -Message ("LOADED_THREAD_PICKED thread_id=" + $activeThreadId + " count=" + $loadedIds.Count)
704
- $stateEnv = Read-DotEnvFile -Path $envFilePath
705
- $stateEnv["BLUN_TELEGRAM_THREAD_ID"] = $activeThreadId
706
- Write-DotEnvFile -Path $envFilePath -Values $stateEnv
707
- Set-EnvVar "BLUN_TELEGRAM_THREAD_ID" $activeThreadId
708
- Write-DebugStage -Path $debugLogPath -Message "ENV_THREAD_WRITTEN"
709
-
710
- $currentRuntime["thread_id"] = $activeThreadId
743
+ }
744
+ if ($loadedIds.Count -gt 0) {
745
+ $activeThreadId = [string]$loadedIds[$loadedIds.Count - 1]
746
+ $bestThreadScore = [double]::NegativeInfinity
747
+ foreach ($candidate in $loadedIds) {
748
+ $candidateThreadId = [string]$candidate
749
+ if ([string]::IsNullOrWhiteSpace($candidateThreadId)) {
750
+ continue
751
+ }
752
+ $threadScore = 0.0
753
+ try {
754
+ $candidateInfo = Invoke-NodeJsonWithRetry -NodeArgs @($bootstrapScript, "read-thread", "--ws-url", $telegramAppServerWsUrl, "--thread-id", $candidateThreadId) -Attempts 1 -DelayMs 0
755
+ $thread = $candidateInfo.response.result.thread
756
+ if ($null -ne $thread -and $null -ne $thread.createdAt) {
757
+ $threadScore = [double]$thread.createdAt
758
+ if ($threadScore -gt 0 -and $threadScore -lt 1000000000000) {
759
+ $threadScore = $threadScore * 1000
760
+ }
761
+ }
762
+ $threadSource = ""
763
+ $threadStatusType = ""
764
+ if ($null -ne $thread -and $null -ne $thread.source) {
765
+ $threadSource = ([string]$thread.source).ToLowerInvariant()
766
+ }
767
+ if ($null -ne $thread -and $null -ne $thread.status -and $null -ne $thread.status.type) {
768
+ $threadStatusType = ([string]$thread.status.type).ToLowerInvariant()
769
+ }
770
+ if ($threadSource -eq "cli" -and $threadStatusType -eq "active") {
771
+ $threadScore += 1000000000000000
772
+ } elseif ($threadStatusType -eq "active") {
773
+ $threadScore += 900000000000000
774
+ } elseif ($threadSource -eq "cli") {
775
+ $threadScore += 800000000000000
776
+ }
777
+ } catch {
778
+ $threadScore = 0.0
779
+ }
780
+ if ($threadScore -ge $bestThreadScore) {
781
+ $bestThreadScore = $threadScore
782
+ $activeThreadId = $candidateThreadId
783
+ }
784
+ }
785
+ Write-DebugStage -Path $debugLogPath -Message ("LOADED_THREAD_PICKED thread_id=" + $activeThreadId + " count=" + $loadedIds.Count + " score=" + $bestThreadScore)
786
+ $stateEnv = Read-DotEnvFile -Path $envFilePath
787
+ $stateEnv["BLUN_TELEGRAM_THREAD_ID"] = $activeThreadId
788
+ Write-DotEnvFile -Path $envFilePath -Values $stateEnv
789
+ Set-EnvVar "BLUN_TELEGRAM_THREAD_ID" $activeThreadId
790
+ Write-DebugStage -Path $debugLogPath -Message "ENV_THREAD_WRITTEN"
791
+
792
+ $telegramState = Try-GetJsonFile -Path (Join-Path $telegramStateDir "state.json")
793
+ if ($null -ne $telegramState) {
794
+ if ($telegramState.PSObject.Properties.Name.Contains("currentThreadId")) {
795
+ $telegramState.currentThreadId = $activeThreadId
796
+ } else {
797
+ $telegramState | Add-Member -NotePropertyName "currentThreadId" -NotePropertyValue $activeThreadId
798
+ }
799
+ Write-TextFileWithRetry -Path (Join-Path $telegramStateDir "state.json") -Content ($telegramState | ConvertTo-Json -Depth 10)
800
+ Write-DebugStage -Path $debugLogPath -Message "STATE_THREAD_WRITTEN"
801
+ }
802
+
803
+ $currentRuntime["thread_id"] = $activeThreadId
711
804
  Write-TextFileWithRetry -Path $currentRuntimeFile -Content ($currentRuntime | ConvertTo-Json -Depth 6)
712
805
  Write-DebugStage -Path $debugLogPath -Message "CURRENT_RUNTIME_THREAD_WRITTEN"
713
806
 
@@ -749,9 +842,24 @@ if ($useRemoteAppServer) {
749
842
  if ($responderPid -gt 0) { $currentRuntime["responder_pid"] = $responderPid }
750
843
  Write-TextFileWithRetry -Path $currentRuntimeFile -Content ($currentRuntime | ConvertTo-Json -Depth 6)
751
844
  Write-DebugStage -Path $debugLogPath -Message "APP_SERVER_INFO_WRITTEN"
752
- } else {
753
- Write-DebugStage -Path $debugLogPath -Message "LOADED_THREAD_NONE"
754
- }
845
+ } else {
846
+ Write-DebugStage -Path $debugLogPath -Message "LOADED_THREAD_NONE"
847
+ if ($sidecarsStartedEarly) {
848
+ $remoteSessionInfo = [ordered]@{
849
+ ws_url = $telegramAppServerWsUrl
850
+ thread_id = ""
851
+ pid = $backendProcess.Id
852
+ started_at = (Get-Date).ToUniversalTime().ToString("o")
853
+ sidecars = [ordered]@{
854
+ poller = [ordered]@{ pid = Read-PidFileValue -Path (Join-Path $telegramStateDir "poller.pid") }
855
+ dispatcher = [ordered]@{ pid = Read-PidFileValue -Path (Join-Path $telegramStateDir "dispatcher.pid") }
856
+ responder = [ordered]@{ pid = Read-PidFileValue -Path (Join-Path $telegramStateDir "responder.pid") }
857
+ }
858
+ }
859
+ Write-TextFileWithRetry -Path $appServerInfoFile -Content ($remoteSessionInfo | ConvertTo-Json -Depth 6)
860
+ Write-DebugStage -Path $debugLogPath -Message "APP_SERVER_INFO_WRITTEN_UNBOUND"
861
+ }
862
+ }
755
863
  } catch {
756
864
  Write-DebugStage -Path $debugLogPath -Message ("LOADED_THREAD_ERROR " + $_.Exception.Message)
757
865
  }
@@ -195,32 +195,46 @@ $legacyEnv = Read-DotEnvFile -Path $legacyEnvPath
195
195
 
196
196
  $tokenValue = ""
197
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
- }
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 $activeEnv["TELEGRAM_BOT_TOKEN"]) {
202
+ $tokenValue = [string]$activeEnv["TELEGRAM_BOT_TOKEN"]
203
+ $tokenSource = "state env legacy key"
204
+ } elseif (Test-TelegramTokenFormat -Value $legacyEnv["BLUN_TELEGRAM_BOT_TOKEN"]) {
205
+ $tokenValue = [string]$legacyEnv["BLUN_TELEGRAM_BOT_TOKEN"]
206
+ $tokenSource = "legacy env fallback"
207
+ } elseif (Test-TelegramTokenFormat -Value $legacyEnv["TELEGRAM_BOT_TOKEN"]) {
208
+ $tokenValue = [string]$legacyEnv["TELEGRAM_BOT_TOKEN"]
209
+ $tokenSource = "legacy env fallback legacy key"
210
+ }
205
211
  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
212
 
207
213
  $allowedChatIds = ""
208
214
  $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." })
215
+ if (Test-AllowedChatIdsFormat -Value $activeEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"]) {
216
+ $allowedChatIds = [string]$activeEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"]
217
+ $allowedChatSource = "state env"
218
+ } elseif (Test-AllowedChatIdsFormat -Value $activeEnv["TELEGRAM_ALLOWED_CHAT_ID"]) {
219
+ $allowedChatIds = [string]$activeEnv["TELEGRAM_ALLOWED_CHAT_ID"]
220
+ $allowedChatSource = "state env legacy key"
221
+ } elseif (Test-AllowedChatIdsFormat -Value $legacyEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"]) {
222
+ $allowedChatIds = [string]$legacyEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"]
223
+ $allowedChatSource = "legacy env fallback"
224
+ } elseif (Test-AllowedChatIdsFormat -Value $legacyEnv["TELEGRAM_ALLOWED_CHAT_ID"]) {
225
+ $allowedChatIds = [string]$legacyEnv["TELEGRAM_ALLOWED_CHAT_ID"]
226
+ $allowedChatSource = "legacy env fallback legacy key"
227
+ }
228
+ Add-Check -List $checks -Name "allowed_chat_ids" -Status $(if ($allowedChatIds) { "ok" } else { "warn" }) -Detail $(if ($allowedChatIds) { $allowedChatIds } else { "No allowlist set. Telegram currently accepts any chat the bot can see." })
217
229
 
218
230
  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)
231
+ 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 + " pending_reply_timeout_ms=" + [string]$status.pending_reply_timeout_ms)
220
232
  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." })
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)
233
+ Add-Check -List $checks -Name "frontend_owner" -Status $(if ($status.frontend_owner_alive) { "ok" } else { "warn" }) -Detail ("pid=" + [string]$status.frontend_owner_pid + " alive=" + [string]$status.frontend_owner_alive)
234
+ Add-Check -List $checks -Name "queue_notifier" -Status $(if (($null -eq $status.queue_notifier_pid) -or ($status.queue_notifier_alive)) { "ok" } else { "warn" }) -Detail ("pid=" + [string]$status.queue_notifier_pid + " alive=" + [string]$status.queue_notifier_alive)
235
+ 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)
236
+ 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)
237
+ 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
238
 
225
239
  if ($status.last_inbound) {
226
240
  $lastInboundSummary = [string]::Format(
@@ -248,7 +262,7 @@ if ($status.last_outbound) {
248
262
  Add-Check -List $checks -Name "last_outbound" -Status "warn" -Detail "No outbound Telegram message recorded yet."
249
263
  }
250
264
 
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)
265
+ 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 + " parked=" + $status.parked_queue_depth + " submitted=" + $status.submitted_depth + " pending_replies=" + $status.pending_reply_depth + " expired_pending_replies=" + $status.expired_pending_reply_depth)
252
266
 
253
267
  $result = [ordered]@{
254
268
  profile = $status.profile
@@ -7,5 +7,6 @@ BLUN_TELEGRAM_THREAD_ID=
7
7
  BLUN_TELEGRAM_RESUME_TIMEOUT_MS=15000
8
8
  BLUN_TELEGRAM_IDLE_COOLDOWN_MS=15000
9
9
  BLUN_TELEGRAM_DISPATCH_MODE=deferred
10
+ BLUN_TELEGRAM_PROGRESS_RELAY=status
10
11
  BLUN_TELEGRAM_POLL_INTERVAL_MS=5000
11
12
  BLUN_TELEGRAM_INJECT_INTERVAL_MS=15000
@@ -11,6 +11,7 @@ It is intentionally **not** an autonomous answer bot.
11
11
  - keeps private chats and group threads separated
12
12
  - binds a live thread id
13
13
  - injects the next queued Telegram message into that exact live thread only after the session is idle
14
+ - keeps injected Telegram messages visible as pending until the matching answer is sent
14
15
  - sends explicit manual replies from the visible operator session
15
16
  - keeps ambient group noise queued unless it is relevant to that operator
16
17
  - lets escalation-style messages bypass the normal idle queue
@@ -51,6 +52,8 @@ Copy `.env.example` to `.env` in the state directory or export env vars:
51
52
  - `BLUN_TELEGRAM_THREAD_ID`
52
53
  - `BLUN_TELEGRAM_RESUME_TIMEOUT_MS`
53
54
  - `BLUN_TELEGRAM_IDLE_COOLDOWN_MS`
55
+ - `BLUN_TELEGRAM_PENDING_REPLY_TIMEOUT_MS`
56
+ - `BLUN_TELEGRAM_PROGRESS_RELAY` (`status` by default, `commentary` to mirror commentary updates, `off` to disable progress notices)
54
57
  - `BLUN_TELEGRAM_DISPATCH_MODE` (`deferred` by default, `legacy` to restore eager dispatch)
55
58
 
56
59
  ## Tools
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { injectNext } from "./lib/bridge.js";
3
+ import { isCurrentSidecarPid } from "./lib/singleton.js";
3
4
 
4
5
  const intervalMs = Number.parseInt(process.env.BLUN_TELEGRAM_INJECT_INTERVAL_MS || "1500", 10) || 1500;
5
6
  let stopping = false;
@@ -18,6 +19,9 @@ process.on("SIGTERM", () => {
18
19
 
19
20
  async function main() {
20
21
  while (!stopping) {
22
+ if (!isCurrentSidecarPid("dispatcher")) {
23
+ break;
24
+ }
21
25
  try {
22
26
  const result = await injectNext("", { auto: true });
23
27
  if (!["empty", "deferred"].includes(result.status)) {