better-codex 0.1.4 → 0.2.0
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/apps/backend/README.md +43 -2
- package/apps/backend/src/core/app-server.ts +68 -2
- package/apps/backend/src/server.ts +156 -1
- package/apps/backend/src/services/codex-config.ts +561 -0
- package/apps/backend/src/thread-activity/service.ts +47 -0
- package/apps/web/README.md +18 -2
- package/apps/web/src/components/layout/codex-settings.tsx +1208 -0
- package/apps/web/src/components/layout/settings-dialog.tsx +9 -1
- package/apps/web/src/components/layout/virtualized-message-list.tsx +581 -86
- package/apps/web/src/hooks/use-hub-connection.ts +21 -3
- package/apps/web/src/hooks/use-thread-history.ts +94 -5
- package/apps/web/src/services/hub-client.ts +98 -1
- package/apps/web/src/types/index.ts +24 -0
- package/apps/web/src/utils/item-format.ts +55 -9
- package/package.json +1 -1
package/apps/backend/README.md
CHANGED
|
@@ -3,16 +3,31 @@
|
|
|
3
3
|
Local backend that supervises multiple `codex app-server` processes and exposes a
|
|
4
4
|
WebSocket bridge for the web UI.
|
|
5
5
|
|
|
6
|
+
## Requirements
|
|
7
|
+
|
|
8
|
+
- Bun
|
|
9
|
+
- Codex CLI available in `PATH` (or configure `CODEX_BIN`)
|
|
10
|
+
|
|
6
11
|
## Setup
|
|
7
12
|
|
|
8
13
|
1. Install dependencies
|
|
9
14
|
```bash
|
|
10
15
|
bun install
|
|
11
16
|
```
|
|
12
|
-
2. Run the server
|
|
17
|
+
2. Run the server (hot reload)
|
|
13
18
|
```bash
|
|
14
19
|
bun run dev
|
|
15
20
|
```
|
|
21
|
+
3. Or run a single instance
|
|
22
|
+
```bash
|
|
23
|
+
bun run start
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Data locations
|
|
27
|
+
|
|
28
|
+
- Analytics, reviews, and thread index live in `CODEX_HUB_DATA_DIR` (defaults to `~/.codex-hub`).
|
|
29
|
+
- Profiles metadata stored at `CODEX_HUB_DATA_DIR/profiles.json`.
|
|
30
|
+
- Per-profile Codex homes are created under `CODEX_HUB_PROFILES_DIR` (defaults to `~/.codex/profiles`).
|
|
16
31
|
|
|
17
32
|
## Protocol types
|
|
18
33
|
|
|
@@ -30,17 +45,43 @@ bun run generate:protocol
|
|
|
30
45
|
- `CODEX_HUB_DATA_DIR` (default: `~/.codex-hub`)
|
|
31
46
|
- `CODEX_HUB_PROFILES_DIR` (default: `~/.codex/profiles`)
|
|
32
47
|
- `CODEX_HUB_DEFAULT_CODEX_HOME` (default: `~/.codex`)
|
|
48
|
+
- `CODEX_HUB_DEFAULT_CWD` (default: workspace root or process `cwd`)
|
|
49
|
+
- `CODEX_DEFAULT_CWD` (fallback for default cwd)
|
|
33
50
|
- `CODEX_BIN` (default: `codex`)
|
|
34
51
|
- `CODEX_FLAGS` (space-delimited)
|
|
35
52
|
- `CODEX_FLAGS_JSON` (JSON array, preferred)
|
|
36
53
|
- `CODEX_APP_SERVER_FLAGS`
|
|
37
54
|
- `CODEX_APP_SERVER_FLAGS_JSON`
|
|
55
|
+
- `CODEX_HUB_APP_SERVER_STARTUP_TIMEOUT_MS` (default: `15000`)
|
|
56
|
+
- `CODEX_HUB_DEBUG_ROUTES=1` (log non-404 route errors)
|
|
57
|
+
|
|
58
|
+
Notes:
|
|
59
|
+
- Prefer `CODEX_FLAGS_JSON` and `CODEX_APP_SERVER_FLAGS_JSON` to avoid shell quoting issues.
|
|
60
|
+
- `CODEX_HUB_TOKEN` should be set if you want a stable token across restarts.
|
|
38
61
|
|
|
39
62
|
## Endpoints
|
|
40
63
|
|
|
41
64
|
- `GET /health`
|
|
65
|
+
- `GET /config` (returns `{ token }` for the web UI)
|
|
66
|
+
- `GET /analytics/daily?metric=turns_started&days=365&profileId=...&model=...`
|
|
67
|
+
- `GET /reviews?profileId=...&limit=100&offset=0`
|
|
42
68
|
- `GET /profiles`
|
|
43
|
-
- `POST /profiles`
|
|
69
|
+
- `POST /profiles` (optional `{ name }`)
|
|
44
70
|
- `POST /profiles/:profileId/start`
|
|
45
71
|
- `POST /profiles/:profileId/stop`
|
|
72
|
+
- `DELETE /profiles/:profileId`
|
|
73
|
+
- `GET /profiles/:profileId/prompts` (returns prompt names + optional descriptions)
|
|
74
|
+
- `GET /profiles/:profileId/prompts/:name` (returns file contents)
|
|
75
|
+
- `GET /profiles/:profileId/config` (returns config + parsed MCP servers)
|
|
76
|
+
- `PUT /profiles/:profileId/config` (body `{ content: string }`)
|
|
77
|
+
- `PUT /profiles/:profileId/mcp-servers` (body `{ servers: McpServerConfig[] }`)
|
|
78
|
+
- `GET /threads/search?q=...&profileId=...&model=...&status=active|archived&createdAfter=...&createdBefore=...&limit=...&offset=...`
|
|
79
|
+
- `GET /threads/active?profileId=...`
|
|
80
|
+
- `POST /threads/reindex` (body `{ profileId?, limit?, autoStart? }`)
|
|
46
81
|
- `WS /ws?token=...`
|
|
82
|
+
|
|
83
|
+
## Troubleshooting
|
|
84
|
+
|
|
85
|
+
- "Missing hub token" in the web UI: ensure the backend is running and `GET /config` is reachable.
|
|
86
|
+
- "app-server startup timed out": raise `CODEX_HUB_APP_SERVER_STARTUP_TIMEOUT_MS` or run the CLI once manually to warm caches.
|
|
87
|
+
- Profiles not starting: verify the Codex CLI is in `PATH` or set `CODEX_BIN`.
|
|
@@ -31,12 +31,18 @@ export class CodexAppServer extends EventEmitter {
|
|
|
31
31
|
private connection?: JsonRpcConnection
|
|
32
32
|
private ready: Promise<void>
|
|
33
33
|
private resolveReady?: () => void
|
|
34
|
+
private readonly startupTimeoutMs: number
|
|
35
|
+
private readonly startupStderr: string[] = []
|
|
36
|
+
private readonly maxStartupStderr = 20
|
|
37
|
+
private startupComplete = false
|
|
34
38
|
|
|
35
39
|
constructor(private readonly options: CodexAppServerOptions) {
|
|
36
40
|
super()
|
|
37
41
|
this.ready = new Promise((resolve) => {
|
|
38
42
|
this.resolveReady = resolve
|
|
39
43
|
})
|
|
44
|
+
const parsed = Number(process.env.CODEX_HUB_APP_SERVER_STARTUP_TIMEOUT_MS ?? 15000)
|
|
45
|
+
this.startupTimeoutMs = Number.isFinite(parsed) ? Math.max(parsed, 0) : 15000
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
async start(): Promise<void> {
|
|
@@ -73,6 +79,12 @@ export class CodexAppServer extends EventEmitter {
|
|
|
73
79
|
this.emit('serverRequest', message)
|
|
74
80
|
})
|
|
75
81
|
this.connection.on('stderr', (message) => {
|
|
82
|
+
if (!this.startupComplete) {
|
|
83
|
+
this.startupStderr.push(message)
|
|
84
|
+
if (this.startupStderr.length > this.maxStartupStderr) {
|
|
85
|
+
this.startupStderr.shift()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
76
88
|
this.emit('stderr', message)
|
|
77
89
|
})
|
|
78
90
|
this.connection.on('error', (error) => {
|
|
@@ -82,8 +94,17 @@ export class CodexAppServer extends EventEmitter {
|
|
|
82
94
|
this.emit('exit', code)
|
|
83
95
|
})
|
|
84
96
|
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
try {
|
|
98
|
+
await this.initializeWithTimeout()
|
|
99
|
+
this.startupComplete = true
|
|
100
|
+
this.resolveReady?.()
|
|
101
|
+
} catch (error) {
|
|
102
|
+
this.process?.kill()
|
|
103
|
+
this.process = undefined
|
|
104
|
+
this.connection = undefined
|
|
105
|
+
this.emit('error', error instanceof Error ? error : new Error(String(error)))
|
|
106
|
+
throw error
|
|
107
|
+
}
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
async request(method: string, params?: unknown): Promise<unknown> {
|
|
@@ -126,6 +147,51 @@ export class CodexAppServer extends EventEmitter {
|
|
|
126
147
|
})
|
|
127
148
|
this.connection.sendNotification('initialized', {})
|
|
128
149
|
}
|
|
150
|
+
|
|
151
|
+
private async initializeWithTimeout(): Promise<void> {
|
|
152
|
+
if (this.startupTimeoutMs <= 0) {
|
|
153
|
+
await this.initialize()
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
if (!this.process) {
|
|
157
|
+
throw new Error('missing app-server process')
|
|
158
|
+
}
|
|
159
|
+
let timeoutId: NodeJS.Timeout | null = null
|
|
160
|
+
const cleanup: Array<() => void> = []
|
|
161
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
162
|
+
timeoutId = setTimeout(() => {
|
|
163
|
+
reject(new Error(`app-server startup timed out after ${this.startupTimeoutMs}ms`))
|
|
164
|
+
}, this.startupTimeoutMs)
|
|
165
|
+
})
|
|
166
|
+
const exitPromise = new Promise<never>((_, reject) => {
|
|
167
|
+
const onExit = (code: number | null) => {
|
|
168
|
+
reject(new Error(`app-server exited before initialize${code !== null ? ` (code ${code})` : ''}`))
|
|
169
|
+
}
|
|
170
|
+
this.process?.once('exit', onExit)
|
|
171
|
+
cleanup.push(() => this.process?.removeListener('exit', onExit))
|
|
172
|
+
})
|
|
173
|
+
const errorPromise = new Promise<never>((_, reject) => {
|
|
174
|
+
const onError = (error: Error) => reject(error)
|
|
175
|
+
this.process?.once('error', onError)
|
|
176
|
+
cleanup.push(() => this.process?.removeListener('error', onError))
|
|
177
|
+
})
|
|
178
|
+
try {
|
|
179
|
+
await Promise.race([this.initialize(), timeoutPromise, exitPromise, errorPromise])
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (error instanceof Error && error.message.includes('startup timed out')) {
|
|
182
|
+
const stderr = this.startupStderr.join('\n')
|
|
183
|
+
if (stderr) {
|
|
184
|
+
throw new Error(`${error.message}\n\n${stderr}`)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
throw error
|
|
188
|
+
} finally {
|
|
189
|
+
if (timeoutId) {
|
|
190
|
+
clearTimeout(timeoutId)
|
|
191
|
+
}
|
|
192
|
+
cleanup.forEach((fn) => fn())
|
|
193
|
+
}
|
|
194
|
+
}
|
|
129
195
|
}
|
|
130
196
|
|
|
131
197
|
export type CodexAppServerEventsMap = CodexAppServerEvents
|
|
@@ -5,10 +5,12 @@ import { join, extname, basename } from 'node:path'
|
|
|
5
5
|
import { loadConfig } from './config'
|
|
6
6
|
import { CodexSupervisor } from './services/supervisor'
|
|
7
7
|
import { ProfileStore } from './services/profile-store'
|
|
8
|
+
import { readProfileConfig, updateProfileMcpServers, writeProfileConfig, type McpServerConfig } from './services/codex-config'
|
|
8
9
|
import { AnalyticsStore } from './analytics/store'
|
|
9
10
|
import { AnalyticsService } from './analytics/service'
|
|
10
11
|
import { ThreadIndexStore } from './thread-index/store'
|
|
11
12
|
import { ThreadIndexService, type ThreadListItem } from './thread-index/service'
|
|
13
|
+
import { ThreadActivityService } from './thread-activity/service'
|
|
12
14
|
import { ReviewService } from './reviews/service'
|
|
13
15
|
import { ReviewStore } from './reviews/store'
|
|
14
16
|
import type { WsEvent, WsRequest, WsResponse } from './ws/messages'
|
|
@@ -24,6 +26,7 @@ const analytics = new AnalyticsService(new AnalyticsStore(join(config.dataDir, '
|
|
|
24
26
|
analytics.init()
|
|
25
27
|
const threadIndex = new ThreadIndexService(new ThreadIndexStore(join(config.dataDir, 'threads.sqlite')))
|
|
26
28
|
threadIndex.init()
|
|
29
|
+
const threadActivity = new ThreadActivityService()
|
|
27
30
|
const reviews = new ReviewService(new ReviewStore(join(config.dataDir, 'reviews.sqlite')))
|
|
28
31
|
reviews.init()
|
|
29
32
|
|
|
@@ -36,8 +39,25 @@ const sendWsEvent = (event: WsEvent) => {
|
|
|
36
39
|
clients.forEach((client) => client.send(payload))
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
const shouldLogRouteErrors =
|
|
43
|
+
process.env.CODEX_HUB_DEBUG_ROUTES === '1' || process.env.NODE_ENV !== 'production'
|
|
44
|
+
|
|
45
|
+
const formatError = (error: unknown) => {
|
|
46
|
+
if (error instanceof Error) {
|
|
47
|
+
return error.stack ?? `${error.name}: ${error.message}`
|
|
48
|
+
}
|
|
49
|
+
return String(error)
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
const app = new Elysia()
|
|
40
53
|
.use(cors({ origin: true }))
|
|
54
|
+
.onError(({ code, error, request }) => {
|
|
55
|
+
if (!shouldLogRouteErrors || code === 'NOT_FOUND') {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
console.error(`[HubBackend] ${request.method} ${request.url} -> ${code}`)
|
|
59
|
+
console.error(formatError(error))
|
|
60
|
+
})
|
|
41
61
|
.get('/health', () => ({ ok: true }))
|
|
42
62
|
.get('/config', () => ({ token: config.authToken }))
|
|
43
63
|
.get(
|
|
@@ -215,6 +235,94 @@ const app = new Elysia()
|
|
|
215
235
|
}),
|
|
216
236
|
}
|
|
217
237
|
)
|
|
238
|
+
.get(
|
|
239
|
+
'/profiles/:profileId/config',
|
|
240
|
+
async ({ params }) => {
|
|
241
|
+
const profile = profileStore.get(params.profileId)
|
|
242
|
+
if (!profile) {
|
|
243
|
+
return new Response('Profile not found', { status: 404 })
|
|
244
|
+
}
|
|
245
|
+
const snapshot = await readProfileConfig(profile.codexHome)
|
|
246
|
+
return {
|
|
247
|
+
path: snapshot.path,
|
|
248
|
+
codexHome: profile.codexHome,
|
|
249
|
+
content: snapshot.content,
|
|
250
|
+
mcpServers: snapshot.mcpServers,
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
params: t.Object({
|
|
255
|
+
profileId: t.String(),
|
|
256
|
+
}),
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
.put(
|
|
260
|
+
'/profiles/:profileId/config',
|
|
261
|
+
async ({ params, body }) => {
|
|
262
|
+
const profile = profileStore.get(params.profileId)
|
|
263
|
+
if (!profile) {
|
|
264
|
+
return new Response('Profile not found', { status: 404 })
|
|
265
|
+
}
|
|
266
|
+
if (!body || typeof body !== 'object' || typeof (body as { content?: string }).content !== 'string') {
|
|
267
|
+
return new Response('Invalid config payload', { status: 400 })
|
|
268
|
+
}
|
|
269
|
+
const snapshot = await writeProfileConfig(profile.codexHome, (body as { content: string }).content)
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
path: snapshot.path,
|
|
273
|
+
codexHome: profile.codexHome,
|
|
274
|
+
content: snapshot.content,
|
|
275
|
+
mcpServers: snapshot.mcpServers,
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
params: t.Object({
|
|
280
|
+
profileId: t.String(),
|
|
281
|
+
}),
|
|
282
|
+
body: t.Object({
|
|
283
|
+
content: t.String(),
|
|
284
|
+
}),
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
.put(
|
|
288
|
+
'/profiles/:profileId/mcp-servers',
|
|
289
|
+
async ({ params, body }) => {
|
|
290
|
+
const profile = profileStore.get(params.profileId)
|
|
291
|
+
if (!profile) {
|
|
292
|
+
return new Response('Profile not found', { status: 404 })
|
|
293
|
+
}
|
|
294
|
+
if (!body || typeof body !== 'object' || !Array.isArray((body as { servers?: McpServerConfig[] }).servers)) {
|
|
295
|
+
return new Response('Invalid MCP server payload', { status: 400 })
|
|
296
|
+
}
|
|
297
|
+
const servers = (body as { servers: McpServerConfig[] }).servers
|
|
298
|
+
const seenNames = new Set<string>()
|
|
299
|
+
for (const server of servers) {
|
|
300
|
+
if (!server?.name || !/^[a-zA-Z0-9_-]+$/.test(server.name)) {
|
|
301
|
+
return new Response('Invalid MCP server name', { status: 400 })
|
|
302
|
+
}
|
|
303
|
+
if (seenNames.has(server.name)) {
|
|
304
|
+
return new Response('Duplicate MCP server name', { status: 400 })
|
|
305
|
+
}
|
|
306
|
+
seenNames.add(server.name)
|
|
307
|
+
}
|
|
308
|
+
const snapshot = await updateProfileMcpServers(profile.codexHome, servers)
|
|
309
|
+
return {
|
|
310
|
+
ok: true,
|
|
311
|
+
path: snapshot.path,
|
|
312
|
+
codexHome: profile.codexHome,
|
|
313
|
+
content: snapshot.content,
|
|
314
|
+
mcpServers: snapshot.mcpServers,
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
params: t.Object({
|
|
319
|
+
profileId: t.String(),
|
|
320
|
+
}),
|
|
321
|
+
body: t.Object({
|
|
322
|
+
servers: t.Array(t.Any()),
|
|
323
|
+
}),
|
|
324
|
+
}
|
|
325
|
+
)
|
|
218
326
|
.get(
|
|
219
327
|
'/threads/search',
|
|
220
328
|
({ query }) => {
|
|
@@ -251,6 +359,18 @@ const app = new Elysia()
|
|
|
251
359
|
}),
|
|
252
360
|
}
|
|
253
361
|
)
|
|
362
|
+
.get(
|
|
363
|
+
'/threads/active',
|
|
364
|
+
({ query }) => {
|
|
365
|
+
const profileId = typeof query.profileId === 'string' ? query.profileId : undefined
|
|
366
|
+
return { threads: threadActivity.list(profileId) }
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
query: t.Object({
|
|
370
|
+
profileId: t.Optional(t.String()),
|
|
371
|
+
}),
|
|
372
|
+
}
|
|
373
|
+
)
|
|
254
374
|
.post(
|
|
255
375
|
'/threads/reindex',
|
|
256
376
|
async ({ body }) => {
|
|
@@ -351,6 +471,7 @@ const app = new Elysia()
|
|
|
351
471
|
|
|
352
472
|
if (payload.type === 'profile.stop') {
|
|
353
473
|
await supervisor.stop(payload.profileId)
|
|
474
|
+
threadActivity.clearProfile(payload.profileId)
|
|
354
475
|
const response: WsResponse = {
|
|
355
476
|
type: 'profile.stopped',
|
|
356
477
|
profileId: payload.profileId,
|
|
@@ -380,13 +501,30 @@ const app = new Elysia()
|
|
|
380
501
|
threadIndex.recordThreadStart(payload.profileId, thread)
|
|
381
502
|
}
|
|
382
503
|
if (payload.method === 'thread/resume') {
|
|
383
|
-
const thread = (result as { thread?: ThreadListItem }).thread
|
|
504
|
+
const thread = (result as { thread?: ThreadListItem & { turns?: Array<{ id?: string; status?: string }> } }).thread
|
|
384
505
|
threadIndex.recordThreadResume(payload.profileId, thread)
|
|
506
|
+
if (thread?.id) {
|
|
507
|
+
let activeTurnId: string | null = null
|
|
508
|
+
if (Array.isArray(thread.turns)) {
|
|
509
|
+
for (let index = thread.turns.length - 1; index >= 0; index -= 1) {
|
|
510
|
+
if (thread.turns[index]?.status === 'inProgress') {
|
|
511
|
+
activeTurnId = thread.turns[index]?.id ?? null
|
|
512
|
+
break
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (activeTurnId) {
|
|
517
|
+
threadActivity.markStarted(payload.profileId, thread.id, activeTurnId)
|
|
518
|
+
} else {
|
|
519
|
+
threadActivity.markCompleted(payload.profileId, thread.id)
|
|
520
|
+
}
|
|
521
|
+
}
|
|
385
522
|
}
|
|
386
523
|
if (payload.method === 'thread/archive') {
|
|
387
524
|
const params = payload.params as { threadId?: string } | undefined
|
|
388
525
|
if (params?.threadId) {
|
|
389
526
|
threadIndex.recordThreadArchive(payload.profileId, params.threadId)
|
|
527
|
+
threadActivity.markCompleted(payload.profileId, params.threadId)
|
|
390
528
|
}
|
|
391
529
|
}
|
|
392
530
|
analytics.trackRpcResponse({
|
|
@@ -424,6 +562,18 @@ const app = new Elysia()
|
|
|
424
562
|
})
|
|
425
563
|
|
|
426
564
|
supervisor.on('notification', (event) => {
|
|
565
|
+
if (event.method === 'turn/started' && event.params && typeof event.params === 'object') {
|
|
566
|
+
const { threadId, turn } = event.params as { threadId?: string; turn?: { id?: string } }
|
|
567
|
+
if (threadId) {
|
|
568
|
+
threadActivity.markStarted(event.profileId, threadId, turn?.id ?? null)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (event.method === 'turn/completed' && event.params && typeof event.params === 'object') {
|
|
572
|
+
const { threadId } = event.params as { threadId?: string }
|
|
573
|
+
if (threadId) {
|
|
574
|
+
threadActivity.markCompleted(event.profileId, threadId)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
427
577
|
if (event.method === 'item/started' && event.params && typeof event.params === 'object') {
|
|
428
578
|
const { threadId, turnId, item } = event.params as {
|
|
429
579
|
threadId?: string
|
|
@@ -503,6 +653,7 @@ supervisor.on('diagnostic', (event) => {
|
|
|
503
653
|
})
|
|
504
654
|
|
|
505
655
|
supervisor.on('exit', (event) => {
|
|
656
|
+
threadActivity.clearProfile(event.profileId)
|
|
506
657
|
const wsEvent: WsEvent = {
|
|
507
658
|
type: 'profile.exit',
|
|
508
659
|
profileId: event.profileId,
|
|
@@ -512,6 +663,10 @@ supervisor.on('exit', (event) => {
|
|
|
512
663
|
})
|
|
513
664
|
|
|
514
665
|
supervisor.on('error', (event) => {
|
|
666
|
+
if (shouldLogRouteErrors) {
|
|
667
|
+
console.error(`[HubBackend] profile ${event.profileId} error`)
|
|
668
|
+
console.error(formatError(event.error))
|
|
669
|
+
}
|
|
515
670
|
const wsEvent: WsEvent = {
|
|
516
671
|
type: 'profile.error',
|
|
517
672
|
profileId: event.profileId,
|