@blunking/codexlink 0.1.1 → 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
@@ -167,14 +179,32 @@ For internal/private profiles:
167
179
  - give it its own Telegram state directory
168
180
  - do not ship internal agent profiles in the public package
169
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
+ ```
199
+
170
200
  ## What it does
171
201
 
172
202
  - starts one consistent local CLI runtime
173
203
  - writes a launch record into `.codex/runtimes/default/`
174
204
  - keeps Telegram queue state under `.codex/channels/telegram-default/`
175
- - attaches Telegram delivery to the same visible session
176
- - defers automatic Telegram delivery until the foreground session is idle
177
- - 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
178
208
 
179
209
  ## What it does not do
180
210
 
@@ -214,10 +244,16 @@ The bundled plugin lives under `telegram-plugin/` and contains:
214
244
  `blun-codex telegram-plugin` now behaves like a guided setup for normal users:
215
245
 
216
246
  1. check whether Telegram is already configured
217
- 2. ask for missing Bot Token or allowed Chat ID(s)
247
+ 2. ask only for a missing Bot Token
218
248
  3. save everything automatically into the local Telegram state folder
219
249
  4. continue into Telegram mode
220
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
+
221
257
  If something is missing later, `blun-codex telegram-doctor` tells you exactly what is missing and what to run next.
222
258
 
223
259
  ## Notes
package/package.json CHANGED
@@ -1,38 +1,40 @@
1
- {
2
- "name": "@blunking/codexlink",
3
- "version": "0.1.1",
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
+ }
@@ -33,6 +33,31 @@ function Try-GetJsonFile {
33
33
  }
34
34
  }
35
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
+
36
61
  function Ensure-Dir {
37
62
  param([string]$Path)
38
63
  if (-not (Test-Path $Path)) {
@@ -88,7 +113,7 @@ function Get-FreeTcpPort {
88
113
  }
89
114
  }
90
115
 
91
- function Stop-ProcessTree {
116
+ function Stop-ProcessTree {
92
117
  param([int[]]$RootIds)
93
118
 
94
119
  $all = @(Get-CimInstance Win32_Process)
@@ -119,9 +144,9 @@ function Stop-ProcessTree {
119
144
  } catch {
120
145
  }
121
146
  }
122
- }
123
-
124
- function Wait-TcpPort {
147
+ }
148
+
149
+ function Wait-TcpPort {
125
150
  param(
126
151
  [string]$HostName,
127
152
  [int]$Port,
@@ -276,7 +301,7 @@ function Quote-PowerShellLiteral {
276
301
  }
277
302
 
278
303
  $runtimeRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
279
- $profilePath = Join-Path $runtimeRoot ("profiles\" + $Agent.ToLower() + ".json")
304
+ $profilePath = Get-ProfilePath -RuntimeRoot $runtimeRoot -ProfileName $Agent
280
305
  $profile = Get-JsonFile -Path $profilePath
281
306
 
282
307
  $resolvedWorkspace = if ($Workspace) { $Workspace } elseif ($profile.workspace) { $profile.workspace } else { (Get-Location).Path }
@@ -502,13 +527,24 @@ if ($useRemoteAppServer) {
502
527
  if ($telegramAllowedChatId) {
503
528
  $stateEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"] = $telegramAllowedChatId
504
529
  }
505
- $stateEnv["BLUN_TELEGRAM_PLUGIN_MODE"] = $TelegramMode
506
- $stateEnv["BLUN_TELEGRAM_APP_SERVER_WS_URL"] = $telegramAppServerWsUrl
507
- $stateEnv["BLUN_TELEGRAM_THREAD_ID"] = ""
508
- Write-DotEnvFile -Path $envFilePath -Values $stateEnv
509
- Write-DebugStage -Path $debugLogPath -Message ("ENV_WRITTEN ws_url=" + $telegramAppServerWsUrl + " env_file=" + $envFilePath)
510
-
511
- 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
512
548
 
513
549
  $appServerPidFile = Join-Path $agentRuntimeDir "app-server.pid"
514
550
  $appServerInfoFile = Join-Path $agentRuntimeDir "app-server.json"
@@ -520,6 +556,7 @@ if ($useRemoteAppServer) {
520
556
  $oldPids = @()
521
557
  if ($previousRuntime.frontend_host_pid) { $oldPids += [int]$previousRuntime.frontend_host_pid }
522
558
  if ($previousRuntime.app_server_pid) { $oldPids += [int]$previousRuntime.app_server_pid }
559
+ if ($previousRuntime.queue_notifier_pid) { $oldPids += [int]$previousRuntime.queue_notifier_pid }
523
560
  if ($previousRuntime.poller_pid) { $oldPids += [int]$previousRuntime.poller_pid }
524
561
  if ($previousRuntime.dispatcher_pid) { $oldPids += [int]$previousRuntime.dispatcher_pid }
525
562
  if ($previousRuntime.responder_pid) { $oldPids += [int]$previousRuntime.responder_pid }
@@ -641,26 +678,57 @@ if ($PrintOnly) {
641
678
  if ($useRemoteAppServer) {
642
679
  $codexScript = (Get-Command codex).Source
643
680
  $currentRuntimeFile = Join-Path $agentRuntimeDir "current-remote-runtime.json"
644
- $windowTitle = "BLUN Codex Telegram [" + $profile.agent_name + "] " + $telegramAppServerWsUrl
645
- $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 " ")
646
693
  Write-DebugStage -Path $debugLogPath -Message ("FRONTEND_SPAWN command=" + $resumeCommand)
647
694
  $frontendProcess = Start-Process -FilePath "powershell" -WorkingDirectory $resolvedWorkspace -ArgumentList @(
648
695
  "-NoExit",
649
696
  "-Command",
650
697
  $resumeCommand
651
698
  ) -PassThru
652
- $currentRuntime = [ordered]@{
653
- ws_url = $telegramAppServerWsUrl
654
- app_server_pid = $backendProcess.Id
655
- frontend_host_pid = $frontendProcess.Id
656
- 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
657
704
  started_at = (Get-Date).ToUniversalTime().ToString("o")
658
- }
659
- Write-TextFileWithRetry -Path $currentRuntimeFile -Content ($currentRuntime | ConvertTo-Json -Depth 4)
660
- Write-DebugStage -Path $debugLogPath -Message "FRONTEND_SPAWNED"
661
- if ($envFilePath -and $bootstrapScript) {
662
- try {
663
- 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"
664
732
  $loadedIds = @()
665
733
  for ($attempt = 1; $attempt -le 40; $attempt++) {
666
734
  $loaded = Invoke-NodeJsonWithRetry -NodeArgs @($bootstrapScript, "list-loaded", "--ws-url", $telegramAppServerWsUrl) -Attempts 1 -DelayMs 0
@@ -672,17 +740,67 @@ if ($useRemoteAppServer) {
672
740
  if ($attempt -lt 40) {
673
741
  Start-Sleep -Milliseconds 500
674
742
  }
675
- }
676
- if ($loadedIds.Count -gt 0) {
677
- $activeThreadId = [string]$loadedIds[$loadedIds.Count - 1]
678
- Write-DebugStage -Path $debugLogPath -Message ("LOADED_THREAD_PICKED thread_id=" + $activeThreadId + " count=" + $loadedIds.Count)
679
- $stateEnv = Read-DotEnvFile -Path $envFilePath
680
- $stateEnv["BLUN_TELEGRAM_THREAD_ID"] = $activeThreadId
681
- Write-DotEnvFile -Path $envFilePath -Values $stateEnv
682
- Set-EnvVar "BLUN_TELEGRAM_THREAD_ID" $activeThreadId
683
- Write-DebugStage -Path $debugLogPath -Message "ENV_THREAD_WRITTEN"
684
-
685
- $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
686
804
  Write-TextFileWithRetry -Path $currentRuntimeFile -Content ($currentRuntime | ConvertTo-Json -Depth 6)
687
805
  Write-DebugStage -Path $debugLogPath -Message "CURRENT_RUNTIME_THREAD_WRITTEN"
688
806
 
@@ -724,9 +842,24 @@ if ($useRemoteAppServer) {
724
842
  if ($responderPid -gt 0) { $currentRuntime["responder_pid"] = $responderPid }
725
843
  Write-TextFileWithRetry -Path $currentRuntimeFile -Content ($currentRuntime | ConvertTo-Json -Depth 6)
726
844
  Write-DebugStage -Path $debugLogPath -Message "APP_SERVER_INFO_WRITTEN"
727
- } else {
728
- Write-DebugStage -Path $debugLogPath -Message "LOADED_THREAD_NONE"
729
- }
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
+ }
730
863
  } catch {
731
864
  Write-DebugStage -Path $debugLogPath -Message ("LOADED_THREAD_ERROR " + $_.Exception.Message)
732
865
  }
@@ -63,7 +63,7 @@ function Get-OverallStatus {
63
63
  return "ok"
64
64
  }
65
65
 
66
- function Write-DoctorReport {
66
+ function Write-DoctorReport {
67
67
  param(
68
68
  [object]$Result,
69
69
  [string]$TokenSource,
@@ -130,12 +130,37 @@ function Write-DoctorReport {
130
130
  Write-Host ""
131
131
  Write-Host "Die Grundkonfiguration steht, aber es gibt noch Laufzeit-Hinweise." -ForegroundColor Yellow
132
132
  Write-Host "Das ist oft normal, wenn Telegram noch nicht aktiv gestartet wurde oder noch keine Nachricht durchlief."
133
- }
134
- }
135
-
136
- $runtimeRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
137
- $profilePath = Join-Path $runtimeRoot ("profiles\" + $Profile.ToLower() + ".json")
138
- $checks = New-Object 'System.Collections.Generic.List[object]'
133
+ }
134
+ }
135
+
136
+ function Get-ProfilePath {
137
+ param(
138
+ [string]$RuntimeRoot,
139
+ [string]$ProfileName
140
+ )
141
+
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"))
151
+
152
+ foreach ($candidate in $candidates) {
153
+ if ($candidate -and (Test-Path $candidate)) {
154
+ return $candidate
155
+ }
156
+ }
157
+
158
+ return $candidates[-1]
159
+ }
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]'
139
164
 
140
165
  if (Test-Path $profilePath) {
141
166
  Add-Check -List $checks -Name "profile_file" -Status "ok" -Detail $profilePath
@@ -170,32 +195,46 @@ $legacyEnv = Read-DotEnvFile -Path $legacyEnvPath
170
195
 
171
196
  $tokenValue = ""
172
197
  $tokenSource = ""
173
- if (Test-TelegramTokenFormat -Value $activeEnv["BLUN_TELEGRAM_BOT_TOKEN"]) {
174
- $tokenValue = [string]$activeEnv["BLUN_TELEGRAM_BOT_TOKEN"]
175
- $tokenSource = "state env"
176
- } elseif (Test-TelegramTokenFormat -Value $legacyEnv["BLUN_TELEGRAM_BOT_TOKEN"]) {
177
- $tokenValue = [string]$legacyEnv["BLUN_TELEGRAM_BOT_TOKEN"]
178
- $tokenSource = "legacy env fallback"
179
- }
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
+ }
180
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." })
181
212
 
182
213
  $allowedChatIds = ""
183
214
  $allowedChatSource = ""
184
- if (Test-AllowedChatIdsFormat -Value $activeEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"]) {
185
- $allowedChatIds = [string]$activeEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"]
186
- $allowedChatSource = "state env"
187
- } elseif (Test-AllowedChatIdsFormat -Value $legacyEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"]) {
188
- $allowedChatIds = [string]$legacyEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"]
189
- $allowedChatSource = "legacy env fallback"
190
- }
191
- 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." })
192
229
 
193
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." })
194
- 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)
195
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." })
196
- 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)
197
- 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)
198
- 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)
199
238
 
200
239
  if ($status.last_inbound) {
201
240
  $lastInboundSummary = [string]::Format(
@@ -223,7 +262,7 @@ if ($status.last_outbound) {
223
262
  Add-Check -List $checks -Name "last_outbound" -Status "warn" -Detail "No outbound Telegram message recorded yet."
224
263
  }
225
264
 
226
- 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)
227
266
 
228
267
  $result = [ordered]@{
229
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)) {