@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 +56 -38
- package/package.json +40 -38
- package/start-codex-agent.ps1 +179 -71
- package/telegram-doctor.ps1 +34 -20
- 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 +55 -46
- package/telegram-status.ps1 +292 -182
- 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
|
|
@@ -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
|
|
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.
|
|
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
|
@@ -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
|
-
|
|
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
|
-
$
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
704
|
-
$
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
}
|
package/telegram-doctor.ps1
CHANGED
|
@@ -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 $
|
|
202
|
-
$tokenValue = [string]$
|
|
203
|
-
$tokenSource = "
|
|
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 $
|
|
213
|
-
$allowedChatIds = [string]$
|
|
214
|
-
$allowedChatSource = "
|
|
215
|
-
}
|
|
216
|
-
|
|
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 "
|
|
222
|
-
Add-Check -List $checks -Name "
|
|
223
|
-
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)
|
|
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
|
|
@@ -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)) {
|