@blunking/codexlink 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,727 +1,738 @@
1
- param(
2
- [Parameter(Mandatory = $true)]
3
- [string]$Agent,
4
-
5
- [string]$Prompt = "",
6
- [string]$Workspace = "",
7
- [ValidateSet("inherit", "plugin", "off")]
8
- [string]$TelegramMode = "inherit",
9
- [switch]$PrintOnly,
10
- [switch]$RemoteControl,
11
- [string[]]$ExtraArgs = @()
12
- )
13
-
14
- $ErrorActionPreference = "Stop"
15
-
16
- function Get-JsonFile {
17
- param([string]$Path)
18
- if (-not (Test-Path $Path)) {
19
- throw "Profile not found: $Path"
20
- }
21
- return Get-Content -Raw -Path $Path | ConvertFrom-Json
22
- }
23
-
24
- function Try-GetJsonFile {
25
- param([string]$Path)
26
- if (-not (Test-Path $Path)) {
27
- return $null
28
- }
29
- try {
30
- return Get-Content -Raw -Path $Path | ConvertFrom-Json
31
- } catch {
32
- return $null
33
- }
34
- }
35
-
36
- function Ensure-Dir {
37
- param([string]$Path)
38
- if (-not (Test-Path $Path)) {
39
- New-Item -ItemType Directory -Path $Path -Force | Out-Null
40
- }
41
- }
42
-
43
- function Set-EnvVar {
44
- param([string]$Name, [string]$Value)
45
- if ($null -ne $Value -and $Value -ne "") {
46
- [Environment]::SetEnvironmentVariable($Name, $Value, "Process")
47
- }
48
- }
49
-
50
- function Resolve-ConfiguredPath {
51
- param([string]$Value)
52
- if (-not $Value) {
53
- return ""
54
- }
55
- $expanded = [Environment]::ExpandEnvironmentVariables($Value)
56
- if ([System.IO.Path]::IsPathRooted($expanded)) {
57
- return $expanded
58
- }
59
- return [System.IO.Path]::GetFullPath((Join-Path $runtimeRoot $expanded))
60
- }
61
-
62
- function Get-TelegramPluginRoot {
63
- param([string]$RuntimeRoot)
64
-
65
- $candidates = @()
66
- if ($env:BLUN_CODEX_TELEGRAM_PLUGIN_ROOT) {
67
- $candidates += $env:BLUN_CODEX_TELEGRAM_PLUGIN_ROOT
68
- }
69
- $candidates += (Join-Path $RuntimeRoot "telegram-plugin")
70
-
71
- foreach ($candidate in $candidates) {
72
- if (-not $candidate) { continue }
73
- if ((Test-Path (Join-Path $candidate "app-server-cli.js")) -and (Test-Path (Join-Path $candidate "sidecar-manager.js"))) {
74
- return $candidate
75
- }
76
- }
77
-
78
- throw "Unable to locate the Telegram plugin root. Set BLUN_CODEX_TELEGRAM_PLUGIN_ROOT or include telegram-plugin beside the runtime."
79
- }
80
-
81
- function Get-FreeTcpPort {
82
- $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
83
- $listener.Start()
84
- try {
85
- return $listener.LocalEndpoint.Port
86
- } finally {
87
- $listener.Stop()
88
- }
89
- }
90
-
91
- function Stop-ProcessTree {
92
- param([int[]]$RootIds)
93
-
94
- $all = @(Get-CimInstance Win32_Process)
95
- $targets = New-Object System.Collections.Generic.HashSet[int]
96
- $queue = New-Object System.Collections.Generic.Queue[int]
97
-
98
- foreach ($rootId in $RootIds) {
99
- if ($rootId -gt 0 -and $targets.Add($rootId)) {
100
- $queue.Enqueue($rootId)
101
- }
102
- }
103
-
104
- while ($queue.Count -gt 0) {
105
- $current = $queue.Dequeue()
106
- foreach ($proc in $all) {
107
- if ($proc.ParentProcessId -eq $current) {
108
- if ($targets.Add([int]$proc.ProcessId)) {
109
- $queue.Enqueue([int]$proc.ProcessId)
110
- }
111
- }
112
- }
113
- }
114
-
115
- $ordered = @($targets) | Sort-Object -Descending
116
- foreach ($procId in $ordered) {
117
- try {
118
- Stop-Process -Id $procId -Force -ErrorAction Stop
119
- } catch {
120
- }
121
- }
122
- }
123
-
124
- function Wait-TcpPort {
125
- param(
126
- [string]$HostName,
127
- [int]$Port,
128
- [int]$TimeoutMs = 15000
129
- )
130
-
131
- $deadline = [DateTime]::UtcNow.AddMilliseconds($TimeoutMs)
132
- while ([DateTime]::UtcNow -lt $deadline) {
133
- try {
134
- $client = [System.Net.Sockets.TcpClient]::new()
135
- $task = $client.ConnectAsync($HostName, $Port)
136
- $connected = $task.Wait(350)
137
- if ($connected -and $client.Connected) {
138
- $client.Dispose()
139
- return $true
140
- }
141
- $client.Dispose()
142
- } catch {
143
- }
144
- Start-Sleep -Milliseconds 200
145
- }
146
- return $false
147
- }
148
-
149
- function Read-DotEnvFile {
150
- param([string]$Path)
151
- $values = @{}
152
- if (-not (Test-Path $Path)) {
153
- return $values
154
- }
155
- foreach ($line in (Get-Content -Path $Path)) {
156
- if (-not $line) { continue }
157
- if ($line.Trim().StartsWith("#")) { continue }
158
- $parts = $line -split "=", 2
159
- if ($parts.Count -ne 2) { continue }
160
- $values[$parts[0].Trim()] = $parts[1]
161
- }
162
- return $values
163
- }
164
-
165
- function Write-DotEnvFile {
166
- param(
167
- [string]$Path,
168
- [hashtable]$Values
169
- )
170
-
171
- $lines = foreach ($key in ($Values.Keys | Sort-Object)) {
172
- "$key=$($Values[$key])"
173
- }
174
- Set-Content -Path $Path -Value $lines -Encoding UTF8
175
- }
176
-
177
- function Write-TextFileWithRetry {
178
- param(
179
- [string]$Path,
180
- [string]$Content,
181
- [int]$Attempts = 6,
182
- [int]$DelayMs = 120
183
- )
184
- for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
185
- try {
186
- Set-Content -Path $Path -Value $Content -Encoding UTF8
187
- return
188
- } catch {
189
- if ($attempt -eq $Attempts) { throw }
190
- Start-Sleep -Milliseconds $DelayMs
191
- }
192
- }
193
- }
194
-
195
- function Append-TextFileWithRetry {
196
- param(
197
- [string]$Path,
198
- [string]$Content,
199
- [int]$Attempts = 6,
200
- [int]$DelayMs = 120
201
- )
202
- for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
203
- try {
204
- Add-Content -Path $Path -Value $Content -Encoding UTF8
205
- return
206
- } catch {
207
- if ($attempt -eq $Attempts) { throw }
208
- Start-Sleep -Milliseconds $DelayMs
209
- }
210
- }
211
- }
212
-
213
- function Read-PidFileValue {
214
- param([string]$Path)
215
- if (-not (Test-Path $Path)) {
216
- return 0
217
- }
218
- try {
219
- return [int]((Get-Content -Raw -Path $Path).Trim())
220
- } catch {
221
- return 0
222
- }
223
- }
224
-
225
- function Write-DebugStage {
226
- param(
227
- [string]$Path,
228
- [string]$Message
229
- )
230
- $line = ((Get-Date).ToUniversalTime().ToString("o")) + " " + $Message
231
- Append-TextFileWithRetry -Path $Path -Content ($line + "`n")
232
- }
233
-
234
- function Invoke-NodeJsonWithRetry {
235
- param(
236
- [string[]]$NodeArgs,
237
- [int]$Attempts = 12,
238
- [int]$DelayMs = 500
239
- )
240
-
241
- $lastRaw = $null
242
- for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
243
- $stdoutPath = Join-Path $env:TEMP ("blun-codex-node-" + [guid]::NewGuid().ToString() + ".stdout.log")
244
- $stderrPath = Join-Path $env:TEMP ("blun-codex-node-" + [guid]::NewGuid().ToString() + ".stderr.log")
245
- $argLine = ($NodeArgs | ForEach-Object { '"' + ($_ -replace '"', '\"') + '"' }) -join " "
246
- $proc = Start-Process -FilePath "node" -ArgumentList $argLine -WindowStyle Hidden -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -Wait -PassThru
247
- $stdoutText = if (Test-Path $stdoutPath) { Get-Content -Raw $stdoutPath } else { "" }
248
- $stderrText = if (Test-Path $stderrPath) { Get-Content -Raw $stderrPath } else { "" }
249
- Remove-Item $stdoutPath,$stderrPath -Force -ErrorAction SilentlyContinue
250
- $lastRaw = ($stdoutText + $stderrText).Trim()
251
- if ($proc.ExitCode -eq 0 -and $stdoutText.Trim()) {
252
- return ($stdoutText | ConvertFrom-Json)
253
- }
254
- if ($attempt -lt $Attempts) {
255
- Start-Sleep -Milliseconds $DelayMs
256
- }
257
- }
258
-
259
- throw ("Node command failed after retries: " + ($NodeArgs -join " ") + "`n" + ($lastRaw | Out-String))
260
- }
261
-
262
- function Quote-TomlLiteral {
263
- param([string]$Value)
264
- if ($null -eq $Value) {
265
- return $null
266
- }
267
- return "'" + ($Value -replace "'", "''") + "'"
268
- }
269
-
270
- function Quote-PowerShellLiteral {
271
- param([string]$Value)
272
- if ($null -eq $Value) {
273
- return "''"
274
- }
275
- return "'" + ($Value -replace "'", "''") + "'"
276
- }
277
-
278
- $runtimeRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
279
- $profilePath = Join-Path $runtimeRoot ("profiles\" + $Agent.ToLower() + ".json")
280
- $profile = Get-JsonFile -Path $profilePath
281
-
282
- $resolvedWorkspace = if ($Workspace) { $Workspace } elseif ($profile.workspace) { $profile.workspace } else { (Get-Location).Path }
283
- $resolvedWorkspace = Resolve-ConfiguredPath -Value $resolvedWorkspace
284
- if (-not $resolvedWorkspace) {
285
- throw "No workspace configured for agent $Agent"
286
- }
287
-
288
- $agentRuntimeDir = Join-Path $env:USERPROFILE (".codex\runtimes\" + $profile.agent_name)
289
- Ensure-Dir -Path $agentRuntimeDir
290
- $debugLogPath = Join-Path $agentRuntimeDir "launch-debug.log"
291
- Write-DebugStage -Path $debugLogPath -Message ("START agent=" + $profile.agent_name + " telegram_mode=" + $TelegramMode + " print_only=" + [string]$PrintOnly + " remote_control=" + [string]$RemoteControl)
292
-
293
- $launchAt = (Get-Date).ToUniversalTime().ToString("o")
294
- $launchManifest = [ordered]@{
295
- agent_name = $profile.agent_name
296
- display_name = $profile.display_name
297
- workspace = $resolvedWorkspace
298
- model = $profile.model
299
- reasoning_effort = $profile.reasoning_effort
300
- sandbox = $profile.sandbox
301
- approval_policy = $profile.approval_policy
302
- remote_control = [bool]$RemoteControl
303
- prompt = $Prompt
304
- extra_args = @($ExtraArgs)
305
- launched_at = $launchAt
306
- profile_path = $profilePath
307
- }
308
-
309
- $lastLaunchPath = Join-Path $agentRuntimeDir "last-launch.json"
310
- $launchHistoryPath = Join-Path $agentRuntimeDir "launch-history.jsonl"
311
- Write-TextFileWithRetry -Path $lastLaunchPath -Content ($launchManifest | ConvertTo-Json -Depth 8)
312
- Append-TextFileWithRetry -Path $launchHistoryPath -Content (($launchManifest | ConvertTo-Json -Compress -Depth 8) + "`n")
313
-
314
- Set-EnvVar "BLUN_CODEX_AGENT" $profile.agent_name
315
- Set-EnvVar "BLUN_CODEX_DISPLAY_NAME" $profile.display_name
316
- Set-EnvVar "BLUN_CODEX_PROFILE_PATH" $profilePath
317
- Set-EnvVar "BLUN_CODEX_RUNTIME_DIR" $agentRuntimeDir
318
- Set-EnvVar "BLUN_CODEX_LANE" $profile.lane
319
- Set-EnvVar "BLUN_CODEX_MODEL" $profile.model
320
- Set-EnvVar "BLUN_CODEX_REASONING_EFFORT" $profile.reasoning_effort
321
- Set-EnvVar "BLUN_CODEX_PERSONALITY" $profile.personality
322
-
323
- $mnemoInherit = $false
324
- $mnemoCwdOverride = $null
325
- $mnemoEnvOverride = $null
326
-
327
- if ($null -ne $profile.mnemo -and $profile.mnemo.inherit -eq $true) {
328
- $mnemoInherit = $true
329
- }
330
-
331
- if (-not $mnemoInherit -and $null -ne $profile.mnemo) {
332
- $effectiveDefaultAgent = if ($profile.mnemo.default_agent) { $profile.mnemo.default_agent } else { $profile.agent_name }
333
- $effectiveAgent = if ($profile.mnemo.agent) { $profile.mnemo.agent } else { $profile.agent_name }
334
-
335
- Set-EnvVar "MNEMO_DEFAULT_AGENT" $effectiveDefaultAgent
336
- Set-EnvVar "MNEMO_AGENT" $effectiveAgent
337
- Set-EnvVar "MNEMO_DB" $profile.mnemo.db
338
- Set-EnvVar "MNEMO_TENANT_ROOT" $profile.mnemo.tenant_root
339
- Set-EnvVar "MNEMO_OWNER_NAME" $profile.mnemo.owner_name
340
- if ($null -ne $profile.mnemo.timezone_offset_hours) {
341
- Set-EnvVar "MNEMO_TZ_OFFSET_HOURS" ([string]$profile.mnemo.timezone_offset_hours)
342
- }
343
- if ($null -ne $profile.mnemo.local_agents) {
344
- Set-EnvVar "MNEMO_LOCAL_AGENTS" (($profile.mnemo.local_agents) -join ",")
345
- }
346
- Set-EnvVar "MNEMO_DEFAULT_SCOPE" $profile.mnemo.default_scope
347
- Set-EnvVar "MNEMO_SCOPE" $profile.mnemo.scope
348
- $mnemoCwdOverride = $profile.mnemo.cwd
349
-
350
- $mnemoEnvPairs = @()
351
- $mnemoEnvPairs += "MNEMO_DEFAULT_AGENT = $(Quote-TomlLiteral $effectiveDefaultAgent)"
352
- $mnemoEnvPairs += "MNEMO_AGENT = $(Quote-TomlLiteral $effectiveAgent)"
353
- if ($profile.mnemo.db) {
354
- $mnemoEnvPairs += "MNEMO_DB = $(Quote-TomlLiteral $profile.mnemo.db)"
355
- }
356
- if ($profile.mnemo.tenant_root) {
357
- $mnemoEnvPairs += "MNEMO_TENANT_ROOT = $(Quote-TomlLiteral $profile.mnemo.tenant_root)"
358
- }
359
- if ($profile.mnemo.owner_name) {
360
- $mnemoEnvPairs += "MNEMO_OWNER_NAME = $(Quote-TomlLiteral $profile.mnemo.owner_name)"
361
- }
362
- if ($null -ne $profile.mnemo.timezone_offset_hours) {
363
- $mnemoEnvPairs += "MNEMO_TZ_OFFSET_HOURS = $(Quote-TomlLiteral ([string]$profile.mnemo.timezone_offset_hours))"
364
- }
365
- if ($null -ne $profile.mnemo.local_agents -and $profile.mnemo.local_agents.Count -gt 0) {
366
- $mnemoEnvPairs += "MNEMO_LOCAL_AGENTS = $(Quote-TomlLiteral (($profile.mnemo.local_agents) -join ','))"
367
- }
368
- if ($profile.mnemo.default_scope) {
369
- $mnemoEnvPairs += "MNEMO_DEFAULT_SCOPE = $(Quote-TomlLiteral $profile.mnemo.default_scope)"
370
- }
371
- if ($profile.mnemo.scope) {
372
- $mnemoEnvPairs += "MNEMO_SCOPE = $(Quote-TomlLiteral $profile.mnemo.scope)"
373
- }
374
- if ($mnemoEnvPairs.Count -gt 0) {
375
- $mnemoEnvOverride = "mcp_servers.mnemo.env={" + ($mnemoEnvPairs -join ", ") + "}"
376
- }
377
- }
378
-
379
- $telegramEnvOverride = $null
380
- $telegramEnabled = $false
381
- $telegramStateDir = $null
382
- $telegramAllowedChatId = $null
383
- $telegramAppServerWsUrl = $null
384
-
385
- if ($null -ne $profile.telegram) {
386
- $telegramEnabled = ($profile.telegram.enabled -eq $true)
387
- $telegramStateDir = $profile.telegram.state_dir
388
- $telegramAllowedChatId = $profile.telegram.allowed_chat_id
389
- }
390
-
391
- if ($TelegramMode -eq "plugin") {
392
- $telegramEnabled = $true
393
- } elseif ($TelegramMode -eq "off") {
394
- $telegramEnabled = $false
395
- }
396
-
397
- if (-not $telegramStateDir) {
398
- $telegramStateDir = Join-Path $env:USERPROFILE (".codex\channels\telegram-" + $profile.agent_name)
399
- } else {
400
- $telegramStateDir = Resolve-ConfiguredPath -Value $telegramStateDir
401
- }
402
-
403
- if ($telegramEnabled) {
404
- Set-EnvVar "BLUN_TELEGRAM_AGENT_NAME" $profile.agent_name
405
- Set-EnvVar "BLUN_TELEGRAM_STATE_DIR" $telegramStateDir
406
- Set-EnvVar "BLUN_TELEGRAM_ALLOWED_CHAT_ID" $telegramAllowedChatId
407
- Set-EnvVar "BLUN_TELEGRAM_PLUGIN_MODE" $TelegramMode
408
- $telegramEnvPairs = @()
409
- $telegramEnvPairs += "BLUN_TELEGRAM_AGENT_NAME = $(Quote-TomlLiteral $profile.agent_name)"
410
- $telegramEnvPairs += "BLUN_TELEGRAM_STATE_DIR = $(Quote-TomlLiteral $telegramStateDir)"
411
- $telegramEnvPairs += "BLUN_TELEGRAM_PLUGIN_MODE = $(Quote-TomlLiteral $TelegramMode)"
412
- if ($telegramAllowedChatId) {
413
- $telegramEnvPairs += "BLUN_TELEGRAM_ALLOWED_CHAT_ID = $(Quote-TomlLiteral $telegramAllowedChatId)"
414
- }
415
- $telegramEnvOverride = "mcp_servers.codexlink_telegram.env={" + ($telegramEnvPairs -join ", ") + "}"
416
- }
417
-
418
- $useRemoteAppServer = ($TelegramMode -eq "plugin")
419
-
420
- $mnemoOverrides = @()
421
- if ($mnemoCwdOverride) {
422
- $mnemoOverrides += "-c"
423
- $mnemoOverrides += ("mcp_servers.mnemo.cwd=" + (Quote-TomlLiteral $mnemoCwdOverride))
424
- }
425
-
426
- if ($mnemoEnvOverride) {
427
- $mnemoOverrides += "-c"
428
- $mnemoOverrides += $mnemoEnvOverride
429
- }
430
-
431
- $commonOverrides = @() + $mnemoOverrides
432
- if ($telegramEnvOverride) {
433
- $commonOverrides += "-c"
434
- $commonOverrides += $telegramEnvOverride
435
- }
436
-
437
- $codexArgs = @()
438
-
439
- if ($profile.reasoning_effort) {
440
- $codexArgs += "-c"
441
- $codexArgs += ("model_reasoning_effort=""" + $profile.reasoning_effort + """")
442
- }
443
-
444
- if ($profile.personality) {
445
- $codexArgs += "-c"
446
- $codexArgs += ("personality=""" + $profile.personality + """")
447
- }
448
-
449
- if ($profile.sandbox) {
450
- $codexArgs += "--sandbox"
451
- $codexArgs += $profile.sandbox
452
- }
453
-
454
- if ($profile.approval_policy) {
455
- $codexArgs += "-a"
456
- $codexArgs += $profile.approval_policy
457
- }
458
-
459
- if ($ExtraArgs.Count -gt 0) {
460
- $codexArgs += $ExtraArgs
461
- }
462
-
463
- $remoteSessionInfo = $null
464
- $backendArgs = @()
465
- $envFilePath = $null
466
- $appServerInfoFile = $null
467
- $bootstrapScript = $null
468
- $sidecarManager = $null
469
-
470
- if ($useRemoteAppServer) {
471
- Write-DebugStage -Path $debugLogPath -Message "REMOTE_MODE enabled"
472
- $telegramPluginRoot = Get-TelegramPluginRoot -RuntimeRoot $runtimeRoot
473
- $port = Get-FreeTcpPort
474
- $telegramAppServerWsUrl = "ws://127.0.0.1:$port"
475
- $backendArgs = @("app-server", "--listen", $telegramAppServerWsUrl)
476
- if ($PrintOnly) {
477
- Write-DebugStage -Path $debugLogPath -Message ("PRINT_ONLY remote ws_url=" + $telegramAppServerWsUrl)
478
- $remoteSessionInfo = [ordered]@{
479
- ws_url = $telegramAppServerWsUrl
480
- thread_id = "{{THREAD_ID}}"
481
- pid = 0
482
- started_at = $null
483
- preview = $true
484
- }
485
- $codexArgs += "--remote"
486
- $codexArgs += $telegramAppServerWsUrl
487
- } else {
488
- Ensure-Dir -Path $telegramStateDir
489
- $envFilePath = Join-Path $telegramStateDir ".env"
490
- $stateEnv = Read-DotEnvFile -Path $envFilePath
491
- $stateEnv["BLUN_TELEGRAM_AGENT_NAME"] = $profile.agent_name
492
- $stateEnv["BLUN_TELEGRAM_STATE_DIR"] = $telegramStateDir
493
- $stateEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"] = $telegramAllowedChatId
494
- $stateEnv["BLUN_TELEGRAM_PLUGIN_MODE"] = $TelegramMode
495
- $stateEnv["BLUN_TELEGRAM_APP_SERVER_WS_URL"] = $telegramAppServerWsUrl
496
- $stateEnv["BLUN_TELEGRAM_THREAD_ID"] = ""
497
- Write-DotEnvFile -Path $envFilePath -Values $stateEnv
498
- Write-DebugStage -Path $debugLogPath -Message ("ENV_WRITTEN ws_url=" + $telegramAppServerWsUrl + " env_file=" + $envFilePath)
499
-
500
- Set-EnvVar "BLUN_TELEGRAM_APP_SERVER_WS_URL" $telegramAppServerWsUrl
501
-
502
- $appServerPidFile = Join-Path $agentRuntimeDir "app-server.pid"
503
- $appServerInfoFile = Join-Path $agentRuntimeDir "app-server.json"
504
- $currentRuntimeFile = Join-Path $agentRuntimeDir "current-remote-runtime.json"
505
- $appServerStdoutPath = Join-Path $agentRuntimeDir "app-server.stdout.log"
506
- $appServerStderrPath = Join-Path $agentRuntimeDir "app-server.stderr.log"
507
- $previousRuntime = Try-GetJsonFile -Path $currentRuntimeFile
508
- if ($null -ne $previousRuntime) {
509
- $oldPids = @()
510
- if ($previousRuntime.frontend_host_pid) { $oldPids += [int]$previousRuntime.frontend_host_pid }
511
- if ($previousRuntime.app_server_pid) { $oldPids += [int]$previousRuntime.app_server_pid }
512
- if ($previousRuntime.poller_pid) { $oldPids += [int]$previousRuntime.poller_pid }
513
- if ($previousRuntime.dispatcher_pid) { $oldPids += [int]$previousRuntime.dispatcher_pid }
514
- if ($previousRuntime.responder_pid) { $oldPids += [int]$previousRuntime.responder_pid }
515
- if ($oldPids.Count -gt 0) {
516
- Stop-ProcessTree -RootIds $oldPids
517
- Write-DebugStage -Path $debugLogPath -Message ("PREVIOUS_RUNTIME_STOPPED pids=" + (($oldPids | Select-Object -Unique) -join ","))
518
- }
519
- Remove-Item $currentRuntimeFile -Force -ErrorAction SilentlyContinue
520
- }
521
- $codexScript = (Get-Command codex).Source
522
- $codexBaseDir = Split-Path -Parent $codexScript
523
- $codexJs = $null
524
- $vendorCandidates = Get-ChildItem -Path (Join-Path $codexBaseDir "node_modules\@*\codex\bin\codex.js") -File -ErrorAction SilentlyContinue
525
- if ($vendorCandidates) {
526
- $codexJs = $vendorCandidates[0].FullName
527
- } else {
528
- $codexJs = Join-Path $codexBaseDir "node_modules\codex\bin\codex.js"
529
- }
530
- $nodeExe = (Get-Command node).Source
531
- Remove-Item $appServerStdoutPath,$appServerStderrPath -Force -ErrorAction SilentlyContinue
532
- $clearedEnvNames = @(
533
- "BLUN_TELEGRAM_AGENT_NAME",
534
- "BLUN_TELEGRAM_STATE_DIR",
535
- "BLUN_TELEGRAM_ALLOWED_CHAT_ID",
536
- "BLUN_TELEGRAM_PLUGIN_MODE",
537
- "BLUN_TELEGRAM_APP_SERVER_WS_URL",
538
- "BLUN_TELEGRAM_THREAD_ID",
539
- "BLUN_TELEGRAM_BOT_TOKEN",
540
- "BLUN_TELEGRAM_CODEX_BIN"
541
- )
542
- $savedEnv = @{}
543
- foreach ($name in $clearedEnvNames) {
544
- $savedEnv[$name] = [Environment]::GetEnvironmentVariable($name, "Process")
545
- [Environment]::SetEnvironmentVariable($name, "", "Process")
546
- }
547
- try {
548
- $backendProcess = Start-Process -FilePath $nodeExe -ArgumentList @(
549
- $codexJs,
550
- "app-server",
551
- "--listen",
552
- $telegramAppServerWsUrl
553
- ) -WorkingDirectory $resolvedWorkspace -RedirectStandardOutput $appServerStdoutPath -RedirectStandardError $appServerStderrPath -PassThru -WindowStyle Hidden
554
- } finally {
555
- foreach ($name in $clearedEnvNames) {
556
- [Environment]::SetEnvironmentVariable($name, $savedEnv[$name], "Process")
557
- }
558
- }
559
- Set-Content -Path $appServerPidFile -Value "$($backendProcess.Id)`n" -Encoding UTF8
560
- Write-DebugStage -Path $debugLogPath -Message ("APP_SERVER_SPAWNED pid=" + $backendProcess.Id + " ws_url=" + $telegramAppServerWsUrl)
561
-
562
- if (-not (Wait-TcpPort -HostName "127.0.0.1" -Port $port -TimeoutMs 20000)) {
563
- $backendStderrTail = if (Test-Path $appServerStderrPath) { ((Get-Content -LiteralPath $appServerStderrPath -Tail 40) -join " | ") } else { "" }
564
- $backendStdoutTail = if (Test-Path $appServerStdoutPath) { ((Get-Content -LiteralPath $appServerStdoutPath -Tail 40) -join " | ") } else { "" }
565
- if ($backendProcess.HasExited) {
566
- Write-DebugStage -Path $debugLogPath -Message ("APP_SERVER_EXITED exit_code=" + $backendProcess.ExitCode)
567
- }
568
- if ($backendStderrTail) {
569
- Write-DebugStage -Path $debugLogPath -Message ("APP_SERVER_STDERR " + $backendStderrTail)
570
- }
571
- if ($backendStdoutTail) {
572
- Write-DebugStage -Path $debugLogPath -Message ("APP_SERVER_STDOUT " + $backendStdoutTail)
573
- }
574
- Write-DebugStage -Path $debugLogPath -Message "WAIT_TCP_TIMEOUT"
575
- throw "Timed out waiting for app-server to listen on $telegramAppServerWsUrl"
576
- }
577
- Write-DebugStage -Path $debugLogPath -Message "TCP_READY"
578
- $bootstrapScript = Join-Path $telegramPluginRoot "app-server-cli.js"
579
- $sidecarManager = Join-Path $telegramPluginRoot "sidecar-manager.js"
580
- $codexArgs += "--remote"
581
- $codexArgs += $telegramAppServerWsUrl
582
- }
583
- } elseif ($RemoteControl) {
584
- $codexArgs += "remote-control"
585
- } else {
586
- $codexArgs += "-C"
587
- $codexArgs += $resolvedWorkspace
588
- }
589
-
590
- if (-not $useRemoteAppServer) {
591
- $codexArgs = $commonOverrides + $codexArgs
592
- }
593
-
594
- if ($Prompt) {
595
- $codexArgs += $Prompt
596
- }
597
-
598
- if ($PrintOnly) {
599
- [pscustomobject]@{
600
- runtime_root = $runtimeRoot
601
- profile_path = $profilePath
602
- workspace = $resolvedWorkspace
603
- env = @{
604
- BLUN_CODEX_AGENT = $env:BLUN_CODEX_AGENT
605
- BLUN_CODEX_LANE = $env:BLUN_CODEX_LANE
606
- MNEMO_DEFAULT_AGENT = $env:MNEMO_DEFAULT_AGENT
607
- MNEMO_AGENT = $env:MNEMO_AGENT
608
- MNEMO_DB = $env:MNEMO_DB
609
- MNEMO_TENANT_ROOT = $env:MNEMO_TENANT_ROOT
610
- BLUN_TELEGRAM_AGENT_NAME = $env:BLUN_TELEGRAM_AGENT_NAME
611
- BLUN_TELEGRAM_STATE_DIR = $env:BLUN_TELEGRAM_STATE_DIR
612
- BLUN_TELEGRAM_ALLOWED_CHAT_ID = $env:BLUN_TELEGRAM_ALLOWED_CHAT_ID
613
- BLUN_TELEGRAM_APP_SERVER_WS_URL = $env:BLUN_TELEGRAM_APP_SERVER_WS_URL
614
- BLUN_TELEGRAM_THREAD_ID = $env:BLUN_TELEGRAM_THREAD_ID
615
- }
616
- mcp_overrides = @{
617
- mnemo_inherit = $mnemoInherit
618
- mnemo_cwd = $mnemoCwdOverride
619
- mnemo_env = $mnemoEnvOverride
620
- telegram_env = $telegramEnvOverride
621
- telegram_mode = $TelegramMode
622
- }
623
- remote_session = $remoteSessionInfo
624
- backend_command = if ($backendArgs.Count -gt 0) { @("codex") + $backendArgs } else { @() }
625
- command = @("codex") + $codexArgs
626
- } | ConvertTo-Json -Depth 8
627
- exit 0
628
- }
629
-
630
- if ($useRemoteAppServer) {
631
- $codexScript = (Get-Command codex).Source
632
- $currentRuntimeFile = Join-Path $agentRuntimeDir "current-remote-runtime.json"
633
- $windowTitle = "BLUN Codex Telegram [" + $profile.agent_name + "] " + $telegramAppServerWsUrl
634
- $resumeCommand = "$host.UI.RawUI.WindowTitle = " + (Quote-PowerShellLiteral $windowTitle) + "; & " + (Quote-PowerShellLiteral $codexScript) + " " + (($codexArgs | ForEach-Object { Quote-PowerShellLiteral $_ }) -join " ")
635
- Write-DebugStage -Path $debugLogPath -Message ("FRONTEND_SPAWN command=" + $resumeCommand)
636
- $frontendProcess = Start-Process -FilePath "powershell" -WorkingDirectory $resolvedWorkspace -ArgumentList @(
637
- "-NoExit",
638
- "-Command",
639
- $resumeCommand
640
- ) -PassThru
641
- $currentRuntime = [ordered]@{
642
- ws_url = $telegramAppServerWsUrl
643
- app_server_pid = $backendProcess.Id
644
- frontend_host_pid = $frontendProcess.Id
645
- profile = $profile.agent_name
646
- started_at = (Get-Date).ToUniversalTime().ToString("o")
647
- }
648
- Write-TextFileWithRetry -Path $currentRuntimeFile -Content ($currentRuntime | ConvertTo-Json -Depth 4)
649
- Write-DebugStage -Path $debugLogPath -Message "FRONTEND_SPAWNED"
650
- if ($envFilePath -and $bootstrapScript) {
651
- try {
652
- Write-DebugStage -Path $debugLogPath -Message "LOADED_THREAD_WAIT_START"
653
- $loadedIds = @()
654
- for ($attempt = 1; $attempt -le 40; $attempt++) {
655
- $loaded = Invoke-NodeJsonWithRetry -NodeArgs @($bootstrapScript, "list-loaded", "--ws-url", $telegramAppServerWsUrl) -Attempts 1 -DelayMs 0
656
- $loadedIds = @($loaded.data)
657
- if ($loadedIds.Count -gt 0) {
658
- Write-DebugStage -Path $debugLogPath -Message ("LOADED_THREAD_SEEN attempt=" + $attempt + " count=" + $loadedIds.Count)
659
- break
660
- }
661
- if ($attempt -lt 40) {
662
- Start-Sleep -Milliseconds 500
663
- }
664
- }
665
- if ($loadedIds.Count -gt 0) {
666
- $activeThreadId = [string]$loadedIds[$loadedIds.Count - 1]
667
- Write-DebugStage -Path $debugLogPath -Message ("LOADED_THREAD_PICKED thread_id=" + $activeThreadId + " count=" + $loadedIds.Count)
668
- $stateEnv = Read-DotEnvFile -Path $envFilePath
669
- $stateEnv["BLUN_TELEGRAM_THREAD_ID"] = $activeThreadId
670
- Write-DotEnvFile -Path $envFilePath -Values $stateEnv
671
- Set-EnvVar "BLUN_TELEGRAM_THREAD_ID" $activeThreadId
672
- Write-DebugStage -Path $debugLogPath -Message "ENV_THREAD_WRITTEN"
673
-
674
- $currentRuntime["thread_id"] = $activeThreadId
675
- Write-TextFileWithRetry -Path $currentRuntimeFile -Content ($currentRuntime | ConvertTo-Json -Depth 6)
676
- Write-DebugStage -Path $debugLogPath -Message "CURRENT_RUNTIME_THREAD_WRITTEN"
677
-
678
- try {
679
- $verify = Invoke-NodeJsonWithRetry -NodeArgs @($bootstrapScript, "read-thread", "--ws-url", $telegramAppServerWsUrl, "--thread-id", $activeThreadId) -Attempts 2 -DelayMs 350
680
- if ($verify.ok -and $verify.threadId -eq $activeThreadId) {
681
- Write-DebugStage -Path $debugLogPath -Message "VERIFY_THREAD_OK"
682
- } else {
683
- Write-DebugStage -Path $debugLogPath -Message "VERIFY_THREAD_INVALID"
684
- }
685
- } catch {
686
- Write-DebugStage -Path $debugLogPath -Message ("VERIFY_THREAD_WARN " + $_.Exception.Message)
687
- }
688
-
689
- & node $sidecarManager | Out-Null
690
- if ($LASTEXITCODE -ne 0) {
691
- Write-DebugStage -Path $debugLogPath -Message "SIDECAR_MANAGER_FAILED"
692
- throw "Failed to ensure Telegram sidecars."
693
- }
694
- Write-DebugStage -Path $debugLogPath -Message "SIDECAR_MANAGER_OK"
695
-
696
- $pollerPid = Read-PidFileValue -Path (Join-Path $telegramStateDir "poller.pid")
697
- $dispatcherPid = Read-PidFileValue -Path (Join-Path $telegramStateDir "dispatcher.pid")
698
- $responderPid = Read-PidFileValue -Path (Join-Path $telegramStateDir "responder.pid")
699
- $remoteSessionInfo = [ordered]@{
700
- ws_url = $telegramAppServerWsUrl
701
- thread_id = $activeThreadId
702
- pid = $backendProcess.Id
703
- started_at = (Get-Date).ToUniversalTime().ToString("o")
704
- sidecars = [ordered]@{
705
- poller = [ordered]@{ pid = $pollerPid }
706
- dispatcher = [ordered]@{ pid = $dispatcherPid }
707
- responder = [ordered]@{ pid = $responderPid }
708
- }
709
- }
710
- Write-TextFileWithRetry -Path $appServerInfoFile -Content ($remoteSessionInfo | ConvertTo-Json -Depth 6)
711
- if ($pollerPid -gt 0) { $currentRuntime["poller_pid"] = $pollerPid }
712
- if ($dispatcherPid -gt 0) { $currentRuntime["dispatcher_pid"] = $dispatcherPid }
713
- if ($responderPid -gt 0) { $currentRuntime["responder_pid"] = $responderPid }
714
- Write-TextFileWithRetry -Path $currentRuntimeFile -Content ($currentRuntime | ConvertTo-Json -Depth 6)
715
- Write-DebugStage -Path $debugLogPath -Message "APP_SERVER_INFO_WRITTEN"
716
- } else {
717
- Write-DebugStage -Path $debugLogPath -Message "LOADED_THREAD_NONE"
718
- }
719
- } catch {
720
- Write-DebugStage -Path $debugLogPath -Message ("LOADED_THREAD_ERROR " + $_.Exception.Message)
721
- }
722
- }
723
- exit 0
724
- }
725
-
726
- & codex @codexArgs
727
- exit $LASTEXITCODE
1
+ param(
2
+ [Parameter(Mandatory = $true)]
3
+ [string]$Agent,
4
+
5
+ [string]$Prompt = "",
6
+ [string]$Workspace = "",
7
+ [ValidateSet("inherit", "plugin", "off")]
8
+ [string]$TelegramMode = "inherit",
9
+ [switch]$PrintOnly,
10
+ [switch]$RemoteControl,
11
+ [string[]]$ExtraArgs = @()
12
+ )
13
+
14
+ $ErrorActionPreference = "Stop"
15
+
16
+ function Get-JsonFile {
17
+ param([string]$Path)
18
+ if (-not (Test-Path $Path)) {
19
+ throw "Profile not found: $Path"
20
+ }
21
+ return Get-Content -Raw -Path $Path | ConvertFrom-Json
22
+ }
23
+
24
+ function Try-GetJsonFile {
25
+ param([string]$Path)
26
+ if (-not (Test-Path $Path)) {
27
+ return $null
28
+ }
29
+ try {
30
+ return Get-Content -Raw -Path $Path | ConvertFrom-Json
31
+ } catch {
32
+ return $null
33
+ }
34
+ }
35
+
36
+ function Ensure-Dir {
37
+ param([string]$Path)
38
+ if (-not (Test-Path $Path)) {
39
+ New-Item -ItemType Directory -Path $Path -Force | Out-Null
40
+ }
41
+ }
42
+
43
+ function Set-EnvVar {
44
+ param([string]$Name, [string]$Value)
45
+ if ($null -ne $Value -and $Value -ne "") {
46
+ [Environment]::SetEnvironmentVariable($Name, $Value, "Process")
47
+ }
48
+ }
49
+
50
+ function Resolve-ConfiguredPath {
51
+ param([string]$Value)
52
+ if (-not $Value) {
53
+ return ""
54
+ }
55
+ $expanded = [Environment]::ExpandEnvironmentVariables($Value)
56
+ if ([System.IO.Path]::IsPathRooted($expanded)) {
57
+ return $expanded
58
+ }
59
+ return [System.IO.Path]::GetFullPath((Join-Path $runtimeRoot $expanded))
60
+ }
61
+
62
+ function Get-TelegramPluginRoot {
63
+ param([string]$RuntimeRoot)
64
+
65
+ $candidates = @()
66
+ if ($env:BLUN_CODEX_TELEGRAM_PLUGIN_ROOT) {
67
+ $candidates += $env:BLUN_CODEX_TELEGRAM_PLUGIN_ROOT
68
+ }
69
+ $candidates += (Join-Path $RuntimeRoot "telegram-plugin")
70
+
71
+ foreach ($candidate in $candidates) {
72
+ if (-not $candidate) { continue }
73
+ if ((Test-Path (Join-Path $candidate "app-server-cli.js")) -and (Test-Path (Join-Path $candidate "sidecar-manager.js"))) {
74
+ return $candidate
75
+ }
76
+ }
77
+
78
+ throw "Unable to locate the Telegram plugin root. Set BLUN_CODEX_TELEGRAM_PLUGIN_ROOT or include telegram-plugin beside the runtime."
79
+ }
80
+
81
+ function Get-FreeTcpPort {
82
+ $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
83
+ $listener.Start()
84
+ try {
85
+ return $listener.LocalEndpoint.Port
86
+ } finally {
87
+ $listener.Stop()
88
+ }
89
+ }
90
+
91
+ function Stop-ProcessTree {
92
+ param([int[]]$RootIds)
93
+
94
+ $all = @(Get-CimInstance Win32_Process)
95
+ $targets = New-Object System.Collections.Generic.HashSet[int]
96
+ $queue = New-Object System.Collections.Generic.Queue[int]
97
+
98
+ foreach ($rootId in $RootIds) {
99
+ if ($rootId -gt 0 -and $targets.Add($rootId)) {
100
+ $queue.Enqueue($rootId)
101
+ }
102
+ }
103
+
104
+ while ($queue.Count -gt 0) {
105
+ $current = $queue.Dequeue()
106
+ foreach ($proc in $all) {
107
+ if ($proc.ParentProcessId -eq $current) {
108
+ if ($targets.Add([int]$proc.ProcessId)) {
109
+ $queue.Enqueue([int]$proc.ProcessId)
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ $ordered = @($targets) | Sort-Object -Descending
116
+ foreach ($procId in $ordered) {
117
+ try {
118
+ Stop-Process -Id $procId -Force -ErrorAction Stop
119
+ } catch {
120
+ }
121
+ }
122
+ }
123
+
124
+ function Wait-TcpPort {
125
+ param(
126
+ [string]$HostName,
127
+ [int]$Port,
128
+ [int]$TimeoutMs = 15000
129
+ )
130
+
131
+ $deadline = [DateTime]::UtcNow.AddMilliseconds($TimeoutMs)
132
+ while ([DateTime]::UtcNow -lt $deadline) {
133
+ try {
134
+ $client = [System.Net.Sockets.TcpClient]::new()
135
+ $task = $client.ConnectAsync($HostName, $Port)
136
+ $connected = $task.Wait(350)
137
+ if ($connected -and $client.Connected) {
138
+ $client.Dispose()
139
+ return $true
140
+ }
141
+ $client.Dispose()
142
+ } catch {
143
+ }
144
+ Start-Sleep -Milliseconds 200
145
+ }
146
+ return $false
147
+ }
148
+
149
+ function Read-DotEnvFile {
150
+ param([string]$Path)
151
+ $values = @{}
152
+ if (-not (Test-Path $Path)) {
153
+ return $values
154
+ }
155
+ foreach ($line in (Get-Content -Path $Path)) {
156
+ if (-not $line) { continue }
157
+ if ($line.Trim().StartsWith("#")) { continue }
158
+ $parts = $line -split "=", 2
159
+ if ($parts.Count -ne 2) { continue }
160
+ $values[$parts[0].Trim()] = $parts[1]
161
+ }
162
+ return $values
163
+ }
164
+
165
+ function Write-DotEnvFile {
166
+ param(
167
+ [string]$Path,
168
+ [hashtable]$Values
169
+ )
170
+
171
+ $lines = foreach ($key in ($Values.Keys | Sort-Object)) {
172
+ "$key=$($Values[$key])"
173
+ }
174
+ Set-Content -Path $Path -Value $lines -Encoding UTF8
175
+ }
176
+
177
+ function Write-TextFileWithRetry {
178
+ param(
179
+ [string]$Path,
180
+ [string]$Content,
181
+ [int]$Attempts = 6,
182
+ [int]$DelayMs = 120
183
+ )
184
+ for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
185
+ try {
186
+ Set-Content -Path $Path -Value $Content -Encoding UTF8
187
+ return
188
+ } catch {
189
+ if ($attempt -eq $Attempts) { throw }
190
+ Start-Sleep -Milliseconds $DelayMs
191
+ }
192
+ }
193
+ }
194
+
195
+ function Append-TextFileWithRetry {
196
+ param(
197
+ [string]$Path,
198
+ [string]$Content,
199
+ [int]$Attempts = 6,
200
+ [int]$DelayMs = 120
201
+ )
202
+ for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
203
+ try {
204
+ Add-Content -Path $Path -Value $Content -Encoding UTF8
205
+ return
206
+ } catch {
207
+ if ($attempt -eq $Attempts) { throw }
208
+ Start-Sleep -Milliseconds $DelayMs
209
+ }
210
+ }
211
+ }
212
+
213
+ function Read-PidFileValue {
214
+ param([string]$Path)
215
+ if (-not (Test-Path $Path)) {
216
+ return 0
217
+ }
218
+ try {
219
+ return [int]((Get-Content -Raw -Path $Path).Trim())
220
+ } catch {
221
+ return 0
222
+ }
223
+ }
224
+
225
+ function Write-DebugStage {
226
+ param(
227
+ [string]$Path,
228
+ [string]$Message
229
+ )
230
+ $line = ((Get-Date).ToUniversalTime().ToString("o")) + " " + $Message
231
+ Append-TextFileWithRetry -Path $Path -Content ($line + "`n")
232
+ }
233
+
234
+ function Invoke-NodeJsonWithRetry {
235
+ param(
236
+ [string[]]$NodeArgs,
237
+ [int]$Attempts = 12,
238
+ [int]$DelayMs = 500
239
+ )
240
+
241
+ $lastRaw = $null
242
+ for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
243
+ $stdoutPath = Join-Path $env:TEMP ("blun-codex-node-" + [guid]::NewGuid().ToString() + ".stdout.log")
244
+ $stderrPath = Join-Path $env:TEMP ("blun-codex-node-" + [guid]::NewGuid().ToString() + ".stderr.log")
245
+ $argLine = ($NodeArgs | ForEach-Object { '"' + ($_ -replace '"', '\"') + '"' }) -join " "
246
+ $proc = Start-Process -FilePath "node" -ArgumentList $argLine -WindowStyle Hidden -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -Wait -PassThru
247
+ $stdoutText = if (Test-Path $stdoutPath) { Get-Content -Raw $stdoutPath } else { "" }
248
+ $stderrText = if (Test-Path $stderrPath) { Get-Content -Raw $stderrPath } else { "" }
249
+ Remove-Item $stdoutPath,$stderrPath -Force -ErrorAction SilentlyContinue
250
+ $lastRaw = ($stdoutText + $stderrText).Trim()
251
+ if ($proc.ExitCode -eq 0 -and $stdoutText.Trim()) {
252
+ return ($stdoutText | ConvertFrom-Json)
253
+ }
254
+ if ($attempt -lt $Attempts) {
255
+ Start-Sleep -Milliseconds $DelayMs
256
+ }
257
+ }
258
+
259
+ throw ("Node command failed after retries: " + ($NodeArgs -join " ") + "`n" + ($lastRaw | Out-String))
260
+ }
261
+
262
+ function Quote-TomlLiteral {
263
+ param([string]$Value)
264
+ if ($null -eq $Value) {
265
+ return $null
266
+ }
267
+ return "'" + ($Value -replace "'", "''") + "'"
268
+ }
269
+
270
+ function Quote-PowerShellLiteral {
271
+ param([string]$Value)
272
+ if ($null -eq $Value) {
273
+ return "''"
274
+ }
275
+ return "'" + ($Value -replace "'", "''") + "'"
276
+ }
277
+
278
+ $runtimeRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
279
+ $profilePath = Join-Path $runtimeRoot ("profiles\" + $Agent.ToLower() + ".json")
280
+ $profile = Get-JsonFile -Path $profilePath
281
+
282
+ $resolvedWorkspace = if ($Workspace) { $Workspace } elseif ($profile.workspace) { $profile.workspace } else { (Get-Location).Path }
283
+ $resolvedWorkspace = Resolve-ConfiguredPath -Value $resolvedWorkspace
284
+ if (-not $resolvedWorkspace) {
285
+ throw "No workspace configured for agent $Agent"
286
+ }
287
+
288
+ $agentRuntimeDir = Join-Path $env:USERPROFILE (".codex\runtimes\" + $profile.agent_name)
289
+ Ensure-Dir -Path $agentRuntimeDir
290
+ $debugLogPath = Join-Path $agentRuntimeDir "launch-debug.log"
291
+ Write-DebugStage -Path $debugLogPath -Message ("START agent=" + $profile.agent_name + " telegram_mode=" + $TelegramMode + " print_only=" + [string]$PrintOnly + " remote_control=" + [string]$RemoteControl)
292
+
293
+ $launchAt = (Get-Date).ToUniversalTime().ToString("o")
294
+ $launchManifest = [ordered]@{
295
+ agent_name = $profile.agent_name
296
+ display_name = $profile.display_name
297
+ workspace = $resolvedWorkspace
298
+ model = $profile.model
299
+ reasoning_effort = $profile.reasoning_effort
300
+ sandbox = $profile.sandbox
301
+ approval_policy = $profile.approval_policy
302
+ remote_control = [bool]$RemoteControl
303
+ prompt = $Prompt
304
+ extra_args = @($ExtraArgs)
305
+ launched_at = $launchAt
306
+ profile_path = $profilePath
307
+ }
308
+
309
+ $lastLaunchPath = Join-Path $agentRuntimeDir "last-launch.json"
310
+ $launchHistoryPath = Join-Path $agentRuntimeDir "launch-history.jsonl"
311
+ Write-TextFileWithRetry -Path $lastLaunchPath -Content ($launchManifest | ConvertTo-Json -Depth 8)
312
+ Append-TextFileWithRetry -Path $launchHistoryPath -Content (($launchManifest | ConvertTo-Json -Compress -Depth 8) + "`n")
313
+
314
+ Set-EnvVar "BLUN_CODEX_AGENT" $profile.agent_name
315
+ Set-EnvVar "BLUN_CODEX_DISPLAY_NAME" $profile.display_name
316
+ Set-EnvVar "BLUN_CODEX_PROFILE_PATH" $profilePath
317
+ Set-EnvVar "BLUN_CODEX_RUNTIME_DIR" $agentRuntimeDir
318
+ Set-EnvVar "BLUN_CODEX_LANE" $profile.lane
319
+ Set-EnvVar "BLUN_CODEX_MODEL" $profile.model
320
+ Set-EnvVar "BLUN_CODEX_REASONING_EFFORT" $profile.reasoning_effort
321
+ Set-EnvVar "BLUN_CODEX_PERSONALITY" $profile.personality
322
+
323
+ $mnemoInherit = $false
324
+ $mnemoCwdOverride = $null
325
+ $mnemoEnvOverride = $null
326
+
327
+ if ($null -ne $profile.mnemo -and $profile.mnemo.inherit -eq $true) {
328
+ $mnemoInherit = $true
329
+ }
330
+
331
+ if (-not $mnemoInherit -and $null -ne $profile.mnemo) {
332
+ $effectiveDefaultAgent = if ($profile.mnemo.default_agent) { $profile.mnemo.default_agent } else { $profile.agent_name }
333
+ $effectiveAgent = if ($profile.mnemo.agent) { $profile.mnemo.agent } else { $profile.agent_name }
334
+
335
+ Set-EnvVar "MNEMO_DEFAULT_AGENT" $effectiveDefaultAgent
336
+ Set-EnvVar "MNEMO_AGENT" $effectiveAgent
337
+ Set-EnvVar "MNEMO_DB" $profile.mnemo.db
338
+ Set-EnvVar "MNEMO_TENANT_ROOT" $profile.mnemo.tenant_root
339
+ Set-EnvVar "MNEMO_OWNER_NAME" $profile.mnemo.owner_name
340
+ if ($null -ne $profile.mnemo.timezone_offset_hours) {
341
+ Set-EnvVar "MNEMO_TZ_OFFSET_HOURS" ([string]$profile.mnemo.timezone_offset_hours)
342
+ }
343
+ if ($null -ne $profile.mnemo.local_agents) {
344
+ Set-EnvVar "MNEMO_LOCAL_AGENTS" (($profile.mnemo.local_agents) -join ",")
345
+ }
346
+ Set-EnvVar "MNEMO_DEFAULT_SCOPE" $profile.mnemo.default_scope
347
+ Set-EnvVar "MNEMO_SCOPE" $profile.mnemo.scope
348
+ $mnemoCwdOverride = $profile.mnemo.cwd
349
+
350
+ $mnemoEnvPairs = @()
351
+ $mnemoEnvPairs += "MNEMO_DEFAULT_AGENT = $(Quote-TomlLiteral $effectiveDefaultAgent)"
352
+ $mnemoEnvPairs += "MNEMO_AGENT = $(Quote-TomlLiteral $effectiveAgent)"
353
+ if ($profile.mnemo.db) {
354
+ $mnemoEnvPairs += "MNEMO_DB = $(Quote-TomlLiteral $profile.mnemo.db)"
355
+ }
356
+ if ($profile.mnemo.tenant_root) {
357
+ $mnemoEnvPairs += "MNEMO_TENANT_ROOT = $(Quote-TomlLiteral $profile.mnemo.tenant_root)"
358
+ }
359
+ if ($profile.mnemo.owner_name) {
360
+ $mnemoEnvPairs += "MNEMO_OWNER_NAME = $(Quote-TomlLiteral $profile.mnemo.owner_name)"
361
+ }
362
+ if ($null -ne $profile.mnemo.timezone_offset_hours) {
363
+ $mnemoEnvPairs += "MNEMO_TZ_OFFSET_HOURS = $(Quote-TomlLiteral ([string]$profile.mnemo.timezone_offset_hours))"
364
+ }
365
+ if ($null -ne $profile.mnemo.local_agents -and $profile.mnemo.local_agents.Count -gt 0) {
366
+ $mnemoEnvPairs += "MNEMO_LOCAL_AGENTS = $(Quote-TomlLiteral (($profile.mnemo.local_agents) -join ','))"
367
+ }
368
+ if ($profile.mnemo.default_scope) {
369
+ $mnemoEnvPairs += "MNEMO_DEFAULT_SCOPE = $(Quote-TomlLiteral $profile.mnemo.default_scope)"
370
+ }
371
+ if ($profile.mnemo.scope) {
372
+ $mnemoEnvPairs += "MNEMO_SCOPE = $(Quote-TomlLiteral $profile.mnemo.scope)"
373
+ }
374
+ if ($mnemoEnvPairs.Count -gt 0) {
375
+ $mnemoEnvOverride = "mcp_servers.mnemo.env={" + ($mnemoEnvPairs -join ", ") + "}"
376
+ }
377
+ }
378
+
379
+ $telegramEnvOverride = $null
380
+ $telegramEnabled = $false
381
+ $telegramStateDir = $null
382
+ $telegramAllowedChatId = $null
383
+ $telegramAppServerWsUrl = $null
384
+
385
+ if ($null -ne $profile.telegram) {
386
+ $telegramEnabled = ($profile.telegram.enabled -eq $true)
387
+ $telegramStateDir = $profile.telegram.state_dir
388
+ $telegramAllowedChatId = $profile.telegram.allowed_chat_id
389
+ }
390
+
391
+ if ($TelegramMode -eq "plugin") {
392
+ $telegramEnabled = $true
393
+ } elseif ($TelegramMode -eq "off") {
394
+ $telegramEnabled = $false
395
+ }
396
+
397
+ if (-not $telegramStateDir) {
398
+ $telegramStateDir = Join-Path $env:USERPROFILE (".codex\channels\telegram-" + $profile.agent_name)
399
+ } else {
400
+ $telegramStateDir = Resolve-ConfiguredPath -Value $telegramStateDir
401
+ }
402
+
403
+ $telegramExistingEnv = @{}
404
+ if ($telegramStateDir) {
405
+ Ensure-Dir -Path $telegramStateDir
406
+ $telegramExistingEnv = Read-DotEnvFile -Path (Join-Path $telegramStateDir ".env")
407
+ }
408
+
409
+ if (-not $telegramAllowedChatId -and $telegramExistingEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"]) {
410
+ $telegramAllowedChatId = [string]$telegramExistingEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"]
411
+ }
412
+
413
+ if ($telegramEnabled) {
414
+ Set-EnvVar "BLUN_TELEGRAM_AGENT_NAME" $profile.agent_name
415
+ Set-EnvVar "BLUN_TELEGRAM_STATE_DIR" $telegramStateDir
416
+ Set-EnvVar "BLUN_TELEGRAM_ALLOWED_CHAT_ID" $telegramAllowedChatId
417
+ Set-EnvVar "BLUN_TELEGRAM_PLUGIN_MODE" $TelegramMode
418
+ $telegramEnvPairs = @()
419
+ $telegramEnvPairs += "BLUN_TELEGRAM_AGENT_NAME = $(Quote-TomlLiteral $profile.agent_name)"
420
+ $telegramEnvPairs += "BLUN_TELEGRAM_STATE_DIR = $(Quote-TomlLiteral $telegramStateDir)"
421
+ $telegramEnvPairs += "BLUN_TELEGRAM_PLUGIN_MODE = $(Quote-TomlLiteral $TelegramMode)"
422
+ if ($telegramAllowedChatId) {
423
+ $telegramEnvPairs += "BLUN_TELEGRAM_ALLOWED_CHAT_ID = $(Quote-TomlLiteral $telegramAllowedChatId)"
424
+ }
425
+ $telegramEnvOverride = "mcp_servers.codexlink_telegram.env={" + ($telegramEnvPairs -join ", ") + "}"
426
+ }
427
+
428
+ $useRemoteAppServer = ($TelegramMode -eq "plugin")
429
+
430
+ $mnemoOverrides = @()
431
+ if ($mnemoCwdOverride) {
432
+ $mnemoOverrides += "-c"
433
+ $mnemoOverrides += ("mcp_servers.mnemo.cwd=" + (Quote-TomlLiteral $mnemoCwdOverride))
434
+ }
435
+
436
+ if ($mnemoEnvOverride) {
437
+ $mnemoOverrides += "-c"
438
+ $mnemoOverrides += $mnemoEnvOverride
439
+ }
440
+
441
+ $commonOverrides = @() + $mnemoOverrides
442
+ if ($telegramEnvOverride) {
443
+ $commonOverrides += "-c"
444
+ $commonOverrides += $telegramEnvOverride
445
+ }
446
+
447
+ $codexArgs = @()
448
+
449
+ if ($profile.reasoning_effort) {
450
+ $codexArgs += "-c"
451
+ $codexArgs += ("model_reasoning_effort=""" + $profile.reasoning_effort + """")
452
+ }
453
+
454
+ if ($profile.personality) {
455
+ $codexArgs += "-c"
456
+ $codexArgs += ("personality=""" + $profile.personality + """")
457
+ }
458
+
459
+ if ($profile.sandbox) {
460
+ $codexArgs += "--sandbox"
461
+ $codexArgs += $profile.sandbox
462
+ }
463
+
464
+ if ($profile.approval_policy) {
465
+ $codexArgs += "-a"
466
+ $codexArgs += $profile.approval_policy
467
+ }
468
+
469
+ if ($ExtraArgs.Count -gt 0) {
470
+ $codexArgs += $ExtraArgs
471
+ }
472
+
473
+ $remoteSessionInfo = $null
474
+ $backendArgs = @()
475
+ $envFilePath = $null
476
+ $appServerInfoFile = $null
477
+ $bootstrapScript = $null
478
+ $sidecarManager = $null
479
+
480
+ if ($useRemoteAppServer) {
481
+ Write-DebugStage -Path $debugLogPath -Message "REMOTE_MODE enabled"
482
+ $telegramPluginRoot = Get-TelegramPluginRoot -RuntimeRoot $runtimeRoot
483
+ $port = Get-FreeTcpPort
484
+ $telegramAppServerWsUrl = "ws://127.0.0.1:$port"
485
+ $backendArgs = @("app-server", "--listen", $telegramAppServerWsUrl)
486
+ if ($PrintOnly) {
487
+ Write-DebugStage -Path $debugLogPath -Message ("PRINT_ONLY remote ws_url=" + $telegramAppServerWsUrl)
488
+ $remoteSessionInfo = [ordered]@{
489
+ ws_url = $telegramAppServerWsUrl
490
+ thread_id = "{{THREAD_ID}}"
491
+ pid = 0
492
+ started_at = $null
493
+ preview = $true
494
+ }
495
+ $codexArgs += "--remote"
496
+ $codexArgs += $telegramAppServerWsUrl
497
+ } else {
498
+ $envFilePath = Join-Path $telegramStateDir ".env"
499
+ $stateEnv = Read-DotEnvFile -Path $envFilePath
500
+ $stateEnv["BLUN_TELEGRAM_AGENT_NAME"] = $profile.agent_name
501
+ $stateEnv["BLUN_TELEGRAM_STATE_DIR"] = $telegramStateDir
502
+ if ($telegramAllowedChatId) {
503
+ $stateEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"] = $telegramAllowedChatId
504
+ }
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
512
+
513
+ $appServerPidFile = Join-Path $agentRuntimeDir "app-server.pid"
514
+ $appServerInfoFile = Join-Path $agentRuntimeDir "app-server.json"
515
+ $currentRuntimeFile = Join-Path $agentRuntimeDir "current-remote-runtime.json"
516
+ $appServerStdoutPath = Join-Path $agentRuntimeDir "app-server.stdout.log"
517
+ $appServerStderrPath = Join-Path $agentRuntimeDir "app-server.stderr.log"
518
+ $previousRuntime = Try-GetJsonFile -Path $currentRuntimeFile
519
+ if ($null -ne $previousRuntime) {
520
+ $oldPids = @()
521
+ if ($previousRuntime.frontend_host_pid) { $oldPids += [int]$previousRuntime.frontend_host_pid }
522
+ if ($previousRuntime.app_server_pid) { $oldPids += [int]$previousRuntime.app_server_pid }
523
+ if ($previousRuntime.poller_pid) { $oldPids += [int]$previousRuntime.poller_pid }
524
+ if ($previousRuntime.dispatcher_pid) { $oldPids += [int]$previousRuntime.dispatcher_pid }
525
+ if ($previousRuntime.responder_pid) { $oldPids += [int]$previousRuntime.responder_pid }
526
+ if ($oldPids.Count -gt 0) {
527
+ Stop-ProcessTree -RootIds $oldPids
528
+ Write-DebugStage -Path $debugLogPath -Message ("PREVIOUS_RUNTIME_STOPPED pids=" + (($oldPids | Select-Object -Unique) -join ","))
529
+ }
530
+ Remove-Item $currentRuntimeFile -Force -ErrorAction SilentlyContinue
531
+ }
532
+ $codexScript = (Get-Command codex).Source
533
+ $codexBaseDir = Split-Path -Parent $codexScript
534
+ $codexJs = $null
535
+ $vendorCandidates = Get-ChildItem -Path (Join-Path $codexBaseDir "node_modules\@*\codex\bin\codex.js") -File -ErrorAction SilentlyContinue
536
+ if ($vendorCandidates) {
537
+ $codexJs = $vendorCandidates[0].FullName
538
+ } else {
539
+ $codexJs = Join-Path $codexBaseDir "node_modules\codex\bin\codex.js"
540
+ }
541
+ $nodeExe = (Get-Command node).Source
542
+ Remove-Item $appServerStdoutPath,$appServerStderrPath -Force -ErrorAction SilentlyContinue
543
+ $clearedEnvNames = @(
544
+ "BLUN_TELEGRAM_AGENT_NAME",
545
+ "BLUN_TELEGRAM_STATE_DIR",
546
+ "BLUN_TELEGRAM_ALLOWED_CHAT_ID",
547
+ "BLUN_TELEGRAM_PLUGIN_MODE",
548
+ "BLUN_TELEGRAM_APP_SERVER_WS_URL",
549
+ "BLUN_TELEGRAM_THREAD_ID",
550
+ "BLUN_TELEGRAM_BOT_TOKEN",
551
+ "BLUN_TELEGRAM_CODEX_BIN"
552
+ )
553
+ $savedEnv = @{}
554
+ foreach ($name in $clearedEnvNames) {
555
+ $savedEnv[$name] = [Environment]::GetEnvironmentVariable($name, "Process")
556
+ [Environment]::SetEnvironmentVariable($name, "", "Process")
557
+ }
558
+ try {
559
+ $backendProcess = Start-Process -FilePath $nodeExe -ArgumentList @(
560
+ $codexJs,
561
+ "app-server",
562
+ "--listen",
563
+ $telegramAppServerWsUrl
564
+ ) -WorkingDirectory $resolvedWorkspace -RedirectStandardOutput $appServerStdoutPath -RedirectStandardError $appServerStderrPath -PassThru -WindowStyle Hidden
565
+ } finally {
566
+ foreach ($name in $clearedEnvNames) {
567
+ [Environment]::SetEnvironmentVariable($name, $savedEnv[$name], "Process")
568
+ }
569
+ }
570
+ Set-Content -Path $appServerPidFile -Value "$($backendProcess.Id)`n" -Encoding UTF8
571
+ Write-DebugStage -Path $debugLogPath -Message ("APP_SERVER_SPAWNED pid=" + $backendProcess.Id + " ws_url=" + $telegramAppServerWsUrl)
572
+
573
+ if (-not (Wait-TcpPort -HostName "127.0.0.1" -Port $port -TimeoutMs 20000)) {
574
+ $backendStderrTail = if (Test-Path $appServerStderrPath) { ((Get-Content -LiteralPath $appServerStderrPath -Tail 40) -join " | ") } else { "" }
575
+ $backendStdoutTail = if (Test-Path $appServerStdoutPath) { ((Get-Content -LiteralPath $appServerStdoutPath -Tail 40) -join " | ") } else { "" }
576
+ if ($backendProcess.HasExited) {
577
+ Write-DebugStage -Path $debugLogPath -Message ("APP_SERVER_EXITED exit_code=" + $backendProcess.ExitCode)
578
+ }
579
+ if ($backendStderrTail) {
580
+ Write-DebugStage -Path $debugLogPath -Message ("APP_SERVER_STDERR " + $backendStderrTail)
581
+ }
582
+ if ($backendStdoutTail) {
583
+ Write-DebugStage -Path $debugLogPath -Message ("APP_SERVER_STDOUT " + $backendStdoutTail)
584
+ }
585
+ Write-DebugStage -Path $debugLogPath -Message "WAIT_TCP_TIMEOUT"
586
+ throw "Timed out waiting for app-server to listen on $telegramAppServerWsUrl"
587
+ }
588
+ Write-DebugStage -Path $debugLogPath -Message "TCP_READY"
589
+ $bootstrapScript = Join-Path $telegramPluginRoot "app-server-cli.js"
590
+ $sidecarManager = Join-Path $telegramPluginRoot "sidecar-manager.js"
591
+ $codexArgs += "--remote"
592
+ $codexArgs += $telegramAppServerWsUrl
593
+ }
594
+ } elseif ($RemoteControl) {
595
+ $codexArgs += "remote-control"
596
+ } else {
597
+ $codexArgs += "-C"
598
+ $codexArgs += $resolvedWorkspace
599
+ }
600
+
601
+ if (-not $useRemoteAppServer) {
602
+ $codexArgs = $commonOverrides + $codexArgs
603
+ }
604
+
605
+ if ($Prompt) {
606
+ $codexArgs += $Prompt
607
+ }
608
+
609
+ if ($PrintOnly) {
610
+ [pscustomobject]@{
611
+ runtime_root = $runtimeRoot
612
+ profile_path = $profilePath
613
+ workspace = $resolvedWorkspace
614
+ env = @{
615
+ BLUN_CODEX_AGENT = $env:BLUN_CODEX_AGENT
616
+ BLUN_CODEX_LANE = $env:BLUN_CODEX_LANE
617
+ MNEMO_DEFAULT_AGENT = $env:MNEMO_DEFAULT_AGENT
618
+ MNEMO_AGENT = $env:MNEMO_AGENT
619
+ MNEMO_DB = $env:MNEMO_DB
620
+ MNEMO_TENANT_ROOT = $env:MNEMO_TENANT_ROOT
621
+ BLUN_TELEGRAM_AGENT_NAME = $env:BLUN_TELEGRAM_AGENT_NAME
622
+ BLUN_TELEGRAM_STATE_DIR = $env:BLUN_TELEGRAM_STATE_DIR
623
+ BLUN_TELEGRAM_ALLOWED_CHAT_ID = $env:BLUN_TELEGRAM_ALLOWED_CHAT_ID
624
+ BLUN_TELEGRAM_APP_SERVER_WS_URL = $env:BLUN_TELEGRAM_APP_SERVER_WS_URL
625
+ BLUN_TELEGRAM_THREAD_ID = $env:BLUN_TELEGRAM_THREAD_ID
626
+ }
627
+ mcp_overrides = @{
628
+ mnemo_inherit = $mnemoInherit
629
+ mnemo_cwd = $mnemoCwdOverride
630
+ mnemo_env = $mnemoEnvOverride
631
+ telegram_env = $telegramEnvOverride
632
+ telegram_mode = $TelegramMode
633
+ }
634
+ remote_session = $remoteSessionInfo
635
+ backend_command = if ($backendArgs.Count -gt 0) { @("codex") + $backendArgs } else { @() }
636
+ command = @("codex") + $codexArgs
637
+ } | ConvertTo-Json -Depth 8
638
+ exit 0
639
+ }
640
+
641
+ if ($useRemoteAppServer) {
642
+ $codexScript = (Get-Command codex).Source
643
+ $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 " ")
646
+ Write-DebugStage -Path $debugLogPath -Message ("FRONTEND_SPAWN command=" + $resumeCommand)
647
+ $frontendProcess = Start-Process -FilePath "powershell" -WorkingDirectory $resolvedWorkspace -ArgumentList @(
648
+ "-NoExit",
649
+ "-Command",
650
+ $resumeCommand
651
+ ) -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
657
+ 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"
664
+ $loadedIds = @()
665
+ for ($attempt = 1; $attempt -le 40; $attempt++) {
666
+ $loaded = Invoke-NodeJsonWithRetry -NodeArgs @($bootstrapScript, "list-loaded", "--ws-url", $telegramAppServerWsUrl) -Attempts 1 -DelayMs 0
667
+ $loadedIds = @($loaded.data)
668
+ if ($loadedIds.Count -gt 0) {
669
+ Write-DebugStage -Path $debugLogPath -Message ("LOADED_THREAD_SEEN attempt=" + $attempt + " count=" + $loadedIds.Count)
670
+ break
671
+ }
672
+ if ($attempt -lt 40) {
673
+ Start-Sleep -Milliseconds 500
674
+ }
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
686
+ Write-TextFileWithRetry -Path $currentRuntimeFile -Content ($currentRuntime | ConvertTo-Json -Depth 6)
687
+ Write-DebugStage -Path $debugLogPath -Message "CURRENT_RUNTIME_THREAD_WRITTEN"
688
+
689
+ try {
690
+ $verify = Invoke-NodeJsonWithRetry -NodeArgs @($bootstrapScript, "read-thread", "--ws-url", $telegramAppServerWsUrl, "--thread-id", $activeThreadId) -Attempts 2 -DelayMs 350
691
+ if ($verify.ok -and $verify.threadId -eq $activeThreadId) {
692
+ Write-DebugStage -Path $debugLogPath -Message "VERIFY_THREAD_OK"
693
+ } else {
694
+ Write-DebugStage -Path $debugLogPath -Message "VERIFY_THREAD_INVALID"
695
+ }
696
+ } catch {
697
+ Write-DebugStage -Path $debugLogPath -Message ("VERIFY_THREAD_WARN " + $_.Exception.Message)
698
+ }
699
+
700
+ & node $sidecarManager | Out-Null
701
+ if ($LASTEXITCODE -ne 0) {
702
+ Write-DebugStage -Path $debugLogPath -Message "SIDECAR_MANAGER_FAILED"
703
+ throw "Failed to ensure Telegram sidecars."
704
+ }
705
+ Write-DebugStage -Path $debugLogPath -Message "SIDECAR_MANAGER_OK"
706
+
707
+ $pollerPid = Read-PidFileValue -Path (Join-Path $telegramStateDir "poller.pid")
708
+ $dispatcherPid = Read-PidFileValue -Path (Join-Path $telegramStateDir "dispatcher.pid")
709
+ $responderPid = Read-PidFileValue -Path (Join-Path $telegramStateDir "responder.pid")
710
+ $remoteSessionInfo = [ordered]@{
711
+ ws_url = $telegramAppServerWsUrl
712
+ thread_id = $activeThreadId
713
+ pid = $backendProcess.Id
714
+ started_at = (Get-Date).ToUniversalTime().ToString("o")
715
+ sidecars = [ordered]@{
716
+ poller = [ordered]@{ pid = $pollerPid }
717
+ dispatcher = [ordered]@{ pid = $dispatcherPid }
718
+ responder = [ordered]@{ pid = $responderPid }
719
+ }
720
+ }
721
+ Write-TextFileWithRetry -Path $appServerInfoFile -Content ($remoteSessionInfo | ConvertTo-Json -Depth 6)
722
+ if ($pollerPid -gt 0) { $currentRuntime["poller_pid"] = $pollerPid }
723
+ if ($dispatcherPid -gt 0) { $currentRuntime["dispatcher_pid"] = $dispatcherPid }
724
+ if ($responderPid -gt 0) { $currentRuntime["responder_pid"] = $responderPid }
725
+ Write-TextFileWithRetry -Path $currentRuntimeFile -Content ($currentRuntime | ConvertTo-Json -Depth 6)
726
+ Write-DebugStage -Path $debugLogPath -Message "APP_SERVER_INFO_WRITTEN"
727
+ } else {
728
+ Write-DebugStage -Path $debugLogPath -Message "LOADED_THREAD_NONE"
729
+ }
730
+ } catch {
731
+ Write-DebugStage -Path $debugLogPath -Message ("LOADED_THREAD_ERROR " + $_.Exception.Message)
732
+ }
733
+ }
734
+ exit 0
735
+ }
736
+
737
+ & codex @codexArgs
738
+ exit $LASTEXITCODE