@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 +50 -14
- package/package.json +40 -38
- package/start-codex-agent.ps1 +172 -39
- package/telegram-doctor.ps1 +66 -27
- package/telegram-plugin/.env.example +1 -0
- package/telegram-plugin/README.md +3 -0
- package/telegram-plugin/dispatcher.js +4 -0
- package/telegram-plugin/lib/bridge.js +1550 -86
- package/telegram-plugin/lib/codex.js +142 -21
- package/telegram-plugin/lib/env.js +29 -1
- package/telegram-plugin/lib/paths.js +7 -1
- package/telegram-plugin/lib/sidecars.js +12 -1
- package/telegram-plugin/lib/singleton.js +66 -0
- package/telegram-plugin/lib/storage.js +66 -25
- package/telegram-plugin/lib/telegram.js +8 -0
- package/telegram-plugin/poller.js +4 -0
- package/telegram-plugin/responder.js +4 -0
- package/telegram-setup.ps1 +87 -53
- package/telegram-status.ps1 +292 -157
- package/telegram-title-embed.ps1 +442 -0
- package/telegram-title-watcher.ps1 +454 -0
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
|
-
|
|
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
|
|
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.
|
|
4
|
-
"description": "BLUN CLI launcher with Telegram channel support for one visible session.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"private": false,
|
|
7
|
-
"bin": {
|
|
8
|
-
"blun-codex": "
|
|
9
|
-
"codexlink": "
|
|
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-
|
|
22
|
-
"telegram-
|
|
23
|
-
"telegram-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
}
|
package/start-codex-agent.ps1
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
$
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
679
|
-
$
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
}
|
package/telegram-doctor.ps1
CHANGED
|
@@ -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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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 $
|
|
177
|
-
$tokenValue = [string]$
|
|
178
|
-
$tokenSource = "
|
|
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 $
|
|
188
|
-
$allowedChatIds = [string]$
|
|
189
|
-
$allowedChatSource = "
|
|
190
|
-
}
|
|
191
|
-
|
|
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 "
|
|
197
|
-
Add-Check -List $checks -Name "
|
|
198
|
-
Add-Check -List $checks -Name "
|
|
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
|
|
@@ -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)) {
|